Skip to content

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:

import.meta.glob('/widgets/[!_]*/widget.config.ts', { eager: true })

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 manifest export.
  • 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

(The header's titleColor takes its global default from settings.theme.widgetTitleColor instead of ui.widgetHeader — it's a theme setting. showTitle / showDivider follow the chain above.)

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: an explicit 'mobile' override, then the 'top-bar' icon (the carousel sits on the top bar, so a top-bar shape is the better fallback), then the base.

These only pick a shape. Color never depends on the file: shell/WidgetIcon.tsx renders every icon as a mask tinted by the theme (see Theme & Storage). So icons overrides are only for a genuinely different shape — today just layer-list's simplified mobile glyph.

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.