{"version":3,"sources":["../src/theme.ts"],"sourcesContent":["import { createPushChannel } from './pushChannel';\nimport { protocolRequest } from './sandboxUtils';\nimport { throwOnRefusal } from './protocolRefusal';\nimport { PROTOCOL_THEME, REQUEST_THEME, REQUEST_THEME_CATALOG, THEME, THEME_CATALOG } from './generated/protocol';\nimport { SCHEMES } from './protocolSchemes';\n\n/**\n * The host UI theme polarity, mirrored from the immediately.run host window into\n * the sandbox. Your app can read this to render in step with the host chrome\n * (light / dark).\n *\n * This is the baseline `theme:read` capability — every app may read it. Changing\n * the host theme is a separate, elevated action (`theme:set`), available only to\n * the theme-toggle system app.\n *\n * `HostTheme` is the RESOLVED POLARITY only. The full selection — which theme is\n * active and which of its modes — is {@link HostThemeSelection}.\n */\nexport type HostTheme = 'light' | 'dark';\n\n/**\n * The full host theme selection (HOST_THEMING_SPEC §2/§9.1): the resolved polarity\n * plus the active theme's registry key and resolved mode. Carried on the widened\n * `theme` push. `modeId` is always the RESOLVED mode (never the literal `system` —\n * an app wants to know what is on screen); `modeSelection` is what was chosen.\n */\nexport interface HostThemeSelection {\n  /** Resolved polarity — the same value the legacy `theme` field carried. */\n  theme: HostTheme;\n  /** The host-minted registry key of the active theme. */\n  themeKey: string;\n  /** The resolved active mode id of the active theme. */\n  modeId: string;\n  /**\n   * What the user selected on the mode axis (HOST_THEMING_SPEC §2): a mode id of the\n   * active theme, or the literal `'system'` when the host follows the OS preference.\n   * A theme switcher highlights this; `modeId` stays what is on screen. Against a\n   * host that predates the field it equals `modeId`.\n   */\n  modeSelection: string;\n}\n\n/**\n * The selection assumed before the host reports. The platform default theme is\n * `immediately-run-default` (dark polarity first-paint, matching the shipped\n * provider default — HOST_THEMING_SPEC §3 build-time correction).\n */\nconst DEFAULT_SELECTION: HostThemeSelection = {\n  theme: 'dark',\n  themeKey: 'immediately-run-default',\n  modeId: 'dark',\n  modeSelection: 'dark',\n};\n\n// Read over the transport (SDK_PACKAGING_SPEC §4): the host pushes `theme` and\n// answers `request-theme` (wire format: site-main channelBridge.ts). The parse\n// reads every field so the full selection survives the transport; the\n// polarity-only surface derives from it. `modeSelection` is the one tolerant\n// field: a host that predates it still sends a usable polarity, so its absence\n// falls back to the resolved mode instead of dropping the message.\nconst channel = createPushChannel<HostThemeSelection>({\n  pushType: THEME,\n  requestType: REQUEST_THEME,\n  initial: DEFAULT_SELECTION,\n  parse: (msg) => {\n    if (msg.theme !== 'light' && msg.theme !== 'dark') return undefined;\n    if (typeof msg.themeKey !== 'string' || typeof msg.modeId !== 'string') return undefined;\n    return {\n      theme: msg.theme,\n      themeKey: msg.themeKey,\n      modeId: msg.modeId,\n      modeSelection: typeof msg.modeSelection === 'string' ? msg.modeSelection : msg.modeId,\n    };\n  },\n});\n\n/**\n * Returns the current host theme polarity. Poll this for a one-off read; use\n * {@link onHostThemeChange} or {@link useHostTheme} to react to changes.\n */\nexport const getHostTheme = (): HostTheme => channel.get().theme;\n\n/**\n * Subscribe to host theme polarity changes. The listener is invoked immediately\n * with the current polarity, then again on every change. Returns an unsubscribe fn.\n */\nexport const onHostThemeChange = (listener: (theme: HostTheme) => void): (() => void) =>\n  channel.onChange((sel) => listener(sel.theme));\n\n/**\n * React hook returning the current host theme polarity, re-rendering when it\n * changes. The recommended way to implement an app's own `useTheme`: follow the\n * host, allow a local override.\n */\nexport const useHostTheme = (): HostTheme => channel.use().theme;\n\n/**\n * Returns the current full host theme selection — polarity, active theme key,\n * resolved mode and mode selection. Use {@link useHostThemeSelection} to react to changes.\n */\nexport const getHostThemeSelection = (): HostThemeSelection => channel.get();\n\n/**\n * Subscribe to full host theme selection changes. The listener is invoked\n * immediately with the current selection, then again on every change. Returns an\n * unsubscribe fn.\n */\nexport const onHostThemeSelectionChange = (listener: (selection: HostThemeSelection) => void): (() => void) =>\n  channel.onChange(listener);\n\n/** React hook returning the current full host theme selection. */\nexport const useHostThemeSelection = (): HostThemeSelection => channel.use();\n\n/**\n * One entry of the theme catalogue: a selectable theme and its modes. The\n * catalogue is projected per grant — the baseline `theme:read` projection carries\n * NO source identities (no repo coordinates, no spaceIds), and labels are bounded.\n */\nexport interface ThemeCatalogEntry {\n  themeKey: string;\n  label: string;\n  modes: { id: string; polarity: 'light' | 'dark' }[];\n}\n\n/**\n * The loaded-theme catalogue (HOST_THEMING_SPEC §9.2): every selectable theme, so\n * a theme-aware app can render a picker and match chrome. Pushed on\n * `theme-catalog` / polled with `request-theme-catalog`.\n */\nexport interface ThemeCatalog {\n  themes: ThemeCatalogEntry[];\n}\n\nconst DEFAULT_CATALOG: ThemeCatalog = { themes: [] };\n\nconst catalogChannel = createPushChannel<ThemeCatalog>({\n  pushType: THEME_CATALOG,\n  requestType: REQUEST_THEME_CATALOG,\n  initial: DEFAULT_CATALOG,\n  parse: (msg) => {\n    const themes = msg.themes;\n    if (!Array.isArray(themes)) return undefined;\n    const out: ThemeCatalogEntry[] = [];\n    for (const t of themes as unknown[]) {\n      if (!t || typeof t !== 'object') return undefined;\n      const entry = t as { themeKey?: unknown; label?: unknown; modes?: unknown };\n      if (typeof entry.themeKey !== 'string' || typeof entry.label !== 'string' || !Array.isArray(entry.modes)) {\n        return undefined;\n      }\n      const modes: ThemeCatalogEntry['modes'] = [];\n      for (const m of entry.modes) {\n        if (!m || typeof m !== 'object') return undefined;\n        const mode = m as { id?: unknown; polarity?: unknown };\n        if (typeof mode.id !== 'string' || (mode.polarity !== 'light' && mode.polarity !== 'dark')) {\n          return undefined;\n        }\n        modes.push({ id: mode.id, polarity: mode.polarity });\n      }\n      out.push({ themeKey: entry.themeKey, label: entry.label, modes });\n    }\n    return { themes: out };\n  },\n});\n\n/**\n * Returns the current theme catalogue (themes + their modes). Use\n * {@link useThemeCatalog} to react to changes.\n */\nexport const getThemeCatalog = (): ThemeCatalog => catalogChannel.get();\n\n/**\n * Subscribe to theme catalogue changes. The listener is invoked immediately with\n * the current catalogue, then again on every change. Returns an unsubscribe fn.\n */\nexport const onThemeCatalogChange = (listener: (catalog: ThemeCatalog) => void): (() => void) =>\n  catalogChannel.onChange(listener);\n\n/** React hook returning the current theme catalogue. */\nexport const useThemeCatalog = (): ThemeCatalog => catalogChannel.use();\n\n/**\n * A location the open-bundle picker returned — a repo or a space, with a\n * confined in-bundle path (OPEN_BUNDLE_SPEC §2). Carried to the host's\n * `theme:sources` `add-source` verb.\n */\nexport type ThemeBundleLocation =\n  | { kind: 'repo'; repo: string; ref?: string; path: string }\n  | { kind: 'space'; spaceId: string; path: string };\n\n/**\n * The canonical `theme:set` params (HOST_THEMING_SPEC §9.3). `theme` is the\n * registry key of the theme to select (or the legacy `'light' | 'dark'` polarity —\n * the host disambiguates by the reserved-id rule: legacy iff the value is\n * light/dark AND `mode` is absent). `mode` is one of that theme's modes or\n * `'system'`.\n */\nexport interface SetHostThemeSelectionParams {\n  theme: string;\n  mode?: string;\n}\n\n/** The one `set` call site, so the wire shape is fingerprint-stable. */\nconst setTheme = async (params: SetHostThemeSelectionParams): Promise<void> => {\n  const res = (await protocolRequest(SCHEMES[PROTOCOL_THEME], 'set', [params])) as\n    | { ok: true; data?: unknown }\n    | { ok: false; code?: string; message?: string }\n    | undefined;\n  throwOnRefusal(res, 'setHostTheme failed');\n};\n\n/**\n * Set the host theme selection — the ELEVATED `theme:set` action\n * (HOST_THEMING_SPEC §9.3). `theme` is the registry key of the theme to select,\n * `mode` is one of that theme's modes or `'system'`. The host applies it and\n * re-pushes the new selection to every `theme:read` iframe, so your own\n * {@link useHostThemeSelection} confirms the change (the loop closes with no\n * special case). Only a grant holding `theme:set` (e.g. the theme-toggle system\n * app) may call this; any other app is rejected host-side with a `forbidden`\n * {@link Error} (carrying `.code`), regardless of what the app claims.\n */\nexport const setHostThemeSelection = async (selection: { theme: string; mode: string }): Promise<void> => {\n  await setTheme(selection);\n};\n\n/**\n * Set the host UI theme polarity — the LEGACY form of `theme:set` (§9.3). The host\n * applies that polarity to the CURRENT theme (never resets to the default), then\n * re-pushes. Keep this alias for old-SDK callers; new code should prefer\n * {@link setHostThemeSelection}.\n */\nexport const setHostTheme = async (theme: HostTheme): Promise<void> => {\n  await setTheme({ theme });\n};\n\n/**\n * Add a theme source — the ELEVATED `theme:sources` `add-source` verb\n * (HOST_THEMING_SPEC §9.3). `location` must be a location the host journal saw a\n * RECENT open-bundle invocation OF THIS APP return (the picker-provenance rule —\n * \"the pick is the consent\", machine-checked); anything else is rejected with a\n * readable reason. The host fetches, gates, and registers the theme before\n * returning, so a rejected pick surfaces inline in the switcher. Only a grant\n * holding `theme:sources` (the theme switcher) may call this.\n */\nexport const addThemeSource = async (location: ThemeBundleLocation): Promise<void> => {\n  const res = (await protocolRequest(SCHEMES[PROTOCOL_THEME], 'add-source', [{ location }])) as\n    | { ok: true; data?: unknown }\n    | { ok: false; code?: string; message?: string }\n    | undefined;\n  throwOnRefusal(res, 'addThemeSource failed');\n};\n\n/**\n * Remove a theme source — the `theme:sources` `remove-source` verb (§9.3). If the\n * removed theme is the current selection, the host falls back to the default theme\n * (keeping the mode selection where it exists). The default theme is never\n * removable. Only a grant holding `theme:sources` may call this.\n */\nexport const removeThemeSource = async (themeKey: string): Promise<void> => {\n  const res = (await protocolRequest(SCHEMES[PROTOCOL_THEME], 'remove-source', [{ themeKey }])) as\n    | { ok: true; data?: unknown }\n    | { ok: false; code?: string; message?: string }\n    | undefined;\n  throwOnRefusal(res, 'removeThemeSource failed');\n};\n"],"mappings":"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","names":[]}