// Host-integration surface — not the author-facing widget API. See host.js. import type { WidgetPropertySchema } from "./index"; /** * Host render-boundary helper: fills a widget's propertySchema `default`s onto * props for unset (undefined/null) leaves; explicitly-bound values pass * through. Never validates or coerces; always returns the merged object. * Applied by the platform hosts, never by widget authors. */ export function resolveProps>( schema: WidgetPropertySchema, props: unknown, ): T; // sc-3727: a `gradient` token's value, the one non-scalar token type. export interface ThemeComponentGradient { from: string; to: string; angle: number; } export type ThemeComponentStyle = Record< string, string | number | ThemeComponentGradient >; export type ThemeComponents = Record; // REQ-THEME-ELEMENT: app-wide style values keyed by widget MANIFEST ID, then by // that widget's own styleSchema field name. Values are structural (the // authoritative type is the widget's own schema), so this mirrors // ThemeComponentStyle rather than naming a closed vocabulary. export type ThemeWidgetStyles = Record; /** * REQ-THEME-15 host helper: validates a raw `themeConfig.components` blob down * to `CONTRACT.themeComponents` — unknown scopes/tokens and malformed values are * dropped. Called when a host folds the tenant theme into the resolved widget * theme, so `theme.components` is always clean. */ export function normaliseThemeComponents(raw: unknown): ThemeComponents; /** * REQ-THEME-ELEMENT host helper: validates a raw `themeConfig.widgetStyles` blob. * Structural only — the key space is the workspace's widget catalog, not the * contract, so the authoritative field type is the widget's own styleSchema. * Bounded by `CONTRACT.themeWidgetStyles`. */ /** * sc-5646 host helper: validates ONE widget style object — a placed node's * `props.style` or an app-wide `widgetStyles[id]` entry. Both scopes hold the * same vocabulary, so both are validated the same way; `maxFields` applies the * per-widget cap only the app-wide map needs. */ export function normaliseWidgetStyleFields( raw: unknown, options?: { maxFields?: number }, ): Record; export function normaliseWidgetStyles(raw: unknown): ThemeWidgetStyles; /** * REQ-THEME-15 host render-boundary helper: folds the widget's declared * `styleSchema` defaults and the theme's per-component tokens into a widget's * props as `style` DEFAULTS, with the author's per-instance values winning. * * sc-6750 — precedence, weakest first: `styleSchema` default -> * palette/`components.` -> `widgetStyles[manifestId]` -> per-instance * `props.style`. Returns the same `props` reference when neither the theme nor * the schema sets anything. Applied by the platform hosts, never by authors. */ export function applyThemeComponentStyle>( manifestId: string, theme: | { components?: ThemeComponents; widgetStyles?: ThemeWidgetStyles } | null | undefined, props: T, styleSchema?: Record | null, ): T; // sc-4939 — the host half of `useToast()`. A widget only ever calls the hook; // these are what a platform host puts behind `WidgetContext.toast`. export type ToastKind = "success" | "error" | "warning" | "info"; export interface ToastPayload { kind?: ToastKind | string; message?: string; } export interface HostToast { id: string; kind: ToastKind; message: string; } /** Resolved values a host paints one toast with. `elevation` is the React * Native style object; `boxShadow` is the CSS string derived from it. */ export interface ToastTokens { kind: ToastKind; accent: string; surface: string; text: string; border: string; radius: number; padding: number; gap: number; stackGap: number; accentBarWidth: number; fontFamily?: string; fontSize: number; elevation: Record; boxShadow: string; } export interface ToastController { /** Enqueue a toast. Returns its id, or null when the message is empty. */ show(payload: ToastPayload): string | null; dismiss(id: string): void; getToasts(): HostToast[]; /** Subscribe to the queue; returns an unsubscribe function. */ subscribe(listener: (toasts: HostToast[]) => void): () => void; /** Clear every pending timer and listener. */ destroy(): void; } export interface ToastControllerOptions { durationMs?: number; maxVisible?: number; setTimer?: (fn: () => void, ms: number) => unknown; clearTimer?: (handle: unknown) => void; } export const TOAST_DEFAULTS: { durationMs: number; maxVisible: number }; export function normalizeToastKind(kind: unknown): ToastKind; export function resolveToastTokens( theme: unknown, kind: ToastKind | string, ): ToastTokens; /** * The host-side toast queue: newest-first stacking capped at `maxVisible`, * auto-dismiss after `durationMs`, injectable timers. Shared by the web Player * and the compiler's native WidgetHost so the two cannot drift. */ export function createToastController( opts?: ToastControllerOptions, ): ToastController; // REQ-NAV-STRUCTURE — the navigation SHAPE a host draws its chrome in. The // vocabulary is closed by `CONTRACT.themeMenuTypes`; since sc-7352 it holds // the sidebar alone (a top bar is a page's local bar, REQ-NAV-LOCAL), and any // stored value outside it resolves to `sidebar`. export type ThemeMenuType = "sidebar"; export interface ResolvedNavigation { menuType: ThemeMenuType; } /** * Resolves a stored `theme_config.navigation` block. Always returns a usable * shape — a junk, partial, or absent input yields the default sidebar, so a * host never has to guard the value it switches on. */ export function normaliseNavigation(navigation: unknown): ResolvedNavigation; /** * How many menu pages this shape may draw at once, or `null` for no cap. The * sidebar has none — its rail and drawer list every menu page. */ export function menuItemCap(menuType: string): number | null; /** The app's bottom edge — a page's own bottom navigation bar. Every colour is * resolved, so no host keeps a default of its own; a `null` `borderColor` * means no divider is drawn at all. */ export interface FooterTokens { backgroundColor: string; textColor: string; /** The active tab's label, its glyph, and — under "accent" — the edge that * marks it. One colour for all three: a mark and its label disagreeing about * which colour means "you are here" would be two marks, not one. */ activeColor: string; /** `null` when no divider is drawn, which a zero `borderWidth` also means. */ borderColor: string | null; borderWidth: number | null; activeStyle: "filled" | "accent"; /** Whether a tab's glyph sits beside its label or above it (sc-7951). */ tabLayout: "inline" | "stacked"; /** The active tab's surface, or `null` for none — which is what "accent" * resolves to unless the author names one. */ activeSurface: string | null; } /** * Resolves the footer strip's tokens from a whole `theme_config`. It reads * `theme.footer` and NOTHING else (sc-8033): the colours used to fall back to * the sidebar's, so restyling the rail restyled the strip and the two could not * be dressed apart. */ export function resolveFooterTokens(theme: unknown): FooterTokens; /** The sidebar rail's resolved tokens. Unlike the footer's, the colours are * never `null`: the default lives here so the two hosts cannot each keep a * different one. `borderColor` is still nullable — no colour, no divider. */ export interface SidebarTokens { backgroundColor: string; textColor: string; activeColor: string; activeStyle: "filled" | "accent"; borderColor: string | null; borderWidth: number | null; } /** * Resolves the sidebar rail's tokens from a whole `theme_config`. Every default * is what the web Player renders, so the export follows the Player rather than * the other way round — except `backgroundColor`, which sc-6596 moved on BOTH * hosts: an unset surface takes the colour the PAGE shows (the app's * `backgroundColor`, or a fully configured `backgroundGradient`'s start colour, * read at full strength) rather than a flat white. */ export function resolveSidebarTokens(theme: unknown): SidebarTokens; /** The top app bar's resolved tokens. `tintColor` and `titleColor` are separate * because an unthemed bar paints its icons slate and its app name in the brand * colour; an authored `topBar.textColor` drives both. The bar carries NO * divider (sc-7360): a stored `topBar.borderColor` / `borderWidth` is inert. */ export interface TopBarTokens { backgroundColor: string; tintColor: string; titleColor: string; /** The current page's mark — the active link's label, and an active tab's * label plus its indicator. Falls back to the RAIL's `activeColor` and then * to the brand, so an app states its navigation colour once. */ activeColor: string; /** How a local bar marks its current page. `underline` keeps the tab in the * bar's surface behind an indicator; `attached` cuts the bar away under the * current tab so the page shows through it. */ tabStyle: "underline" | "attached"; /** Under `attached`, the colour the PANEL behind the row paints — the * authored `tabActiveBackgroundColor`, alpha kept — or null for the page's * own wash. The current tab paints nothing itself. */ contentSurface: string | null; /** A tab's own surface, authored or null. Null paints none and the bar shows * through. */ tabBackgroundColor: string | null; /** The underline under the current tab in px, 0-8. Zero draws none and * leaves the tab marked by its label colour alone, so the WIDTH is this * one's switch — it shares `activeColor` with the label and so has no null * colour to switch on. */ tabIndicatorWidth: number; /** The tab's TOP corner rounding in px, 0-24. Its feet stay square whatever * this says — a rounded foot notches the join with the content. */ tabCornerRadius: number; /** The room inside a tab in px, 0-32, defaulting to the row's shipped 12/8. */ tabPaddingX: number; tabPaddingY: number; /** The current tab's own surface, authored or null. Under `attached` the * resolved surface is `contentSurface` above, which already honours it. */ tabActiveBackgroundColor: string | null; } /** * Resolves the top app bar's tokens from a whole `theme_config`. `show` is not * among them: it depends on the menu type, not the theme, and is a no-op on * native. An unset `backgroundColor` follows the PAGE's colour, as the rail's * does (sc-6596). */ export function resolveTopBarTokens(theme: unknown): TopBarTokens; /** A value a widget may persist: a scalar, or a flat array of them. */ export type WidgetRouteScalar = string | number | boolean; export type WidgetRouteValue = WidgetRouteScalar | WidgetRouteScalar[]; /** How many entries one array value may carry. */ export const WIDGET_ROUTE_MAX_ITEMS: number; /** Value equality for "is this key still at its declared default" — structural * for arrays, so an array key can be cleared from the address. */ export function sameWidgetRouteValue(a: unknown, b: unknown): boolean; /** Query-string / route-param key prefix for a widget's stored position. */ export const WIDGET_ROUTE_PREFIX: string; /** Cap on ONE instance's encoded bag, so a single widget cannot spend the * whole URL budget. An over-cap write is dropped, never truncated. */ export const WIDGET_ROUTE_MAX_CHARS: number; /** The key one placed widget instance stores its route bag under. */ export function widgetRouteKey(instanceId: string): string; /** Whether a raw query-string key belongs to a widget rather than the page. */ export function isWidgetRouteKey(key: unknown): boolean; /** Encode a bag for the address. Returns "" when there is nothing to store or * the bag exceeds `WIDGET_ROUTE_MAX_CHARS` — the caller removes the key. */ export function encodeWidgetRoute( bag: Record | null | undefined, ): string; /** Decode what the address carried. Total: any malformed input yields `{}`. */ export function decodeWidgetRoute( raw: unknown, ): Record; /** Every widget's stored bag out of a raw query-string map, keyed by instance id. */ export function collectWidgetRoutes( rawParams: Record | null | undefined, ): Record>; /** Longest heading/subheading the sign-in card accepts. */ export const LOGIN_TEXT_MAX_LENGTH: number; export interface ThemeGradient { type: "linear" | "radial"; from: string; to: string; angle: number; stop?: number; } /** The app-wide background gradient, or null when unset/malformed. */ export function normaliseThemeGradient(raw: unknown): ThemeGradient | null; export interface LoginDesign { backgroundColor: string | null; backgroundGradient: ThemeGradient | null; backgroundImageUrl: string | null; backgroundOverlay: string | null; heading: string | null; subheading: string | null; } /** The sign-in screen's authored design, or null when nothing is configured. */ export function normaliseLoginDesign(raw: unknown): LoginDesign | null; /** sc-7800 — true when a stored value is a `theme:` colour binding. */ export function isThemeTokenRef(value: unknown): boolean; /** * Resolve one `theme:` binding against a resolved widget theme. Returns * `undefined` for a non-binding, a token outside `CONTRACT.themeColorTokens`, * or a path that does not resolve to a colour. */ export function resolveThemeTokenRef( value: unknown, theme: unknown, ): string | undefined; /** * Replace every bound `type: "color"` field on a widget's props — both the * top-level `propertySchema` fields and the `styleSchema` fields under * `props.style` — with the colour its token currently resolves to. Returns the * same reference when nothing is bound. */ export function resolveThemeTokenRefs( props: T, theme: unknown, schemas?: { propertySchema?: unknown; styleSchema?: unknown }, ): T;