@nimblebrain/synapse 0.16.0 → 0.17.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +61 -111
- package/dist/{chunk-SN7YXLAV.cjs → chunk-EHLQWCYV.cjs} +6 -6
- package/dist/{chunk-SN7YXLAV.cjs.map → chunk-EHLQWCYV.cjs.map} +1 -1
- package/dist/{chunk-ZMNYUGSF.cjs → chunk-FDMRJZKT.cjs} +576 -662
- package/dist/chunk-FDMRJZKT.cjs.map +1 -0
- package/dist/{chunk-E7OOOYVR.js → chunk-SRQNQ26I.js} +3 -3
- package/dist/{chunk-E7OOOYVR.js.map → chunk-SRQNQ26I.js.map} +1 -1
- package/dist/{chunk-RYWW6EHB.cjs → chunk-TXJMGQYF.cjs} +3 -5
- package/dist/chunk-TXJMGQYF.cjs.map +1 -0
- package/dist/{chunk-J5ICAHK2.js → chunk-UPDBDXQB.js} +4 -5
- package/dist/chunk-UPDBDXQB.js.map +1 -0
- package/dist/{chunk-GNTQJSQX.js → chunk-VYR6V47N.js} +573 -663
- package/dist/chunk-VYR6V47N.js.map +1 -0
- package/dist/codegen/cli.cjs +1 -1
- package/dist/codegen/cli.js +1 -1
- package/dist/codegen/index.d.cts +1 -1
- package/dist/codegen/index.d.ts +1 -1
- package/dist/connect.iife.global.js +28 -28
- package/dist/{detect-C_qmYsh9.d.cts → detect-BHYg26_d.d.cts} +1 -1
- package/dist/{detect-Dfd0upoz.d.ts → detect-Bf8Q_0dK.d.ts} +1 -1
- package/dist/host/index.cjs +14 -14
- package/dist/host/index.d.cts +3 -3
- package/dist/host/index.d.ts +3 -3
- package/dist/host/index.js +2 -2
- package/dist/index.cjs +28 -87
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +58 -26
- package/dist/index.d.ts +58 -26
- package/dist/index.js +3 -81
- package/dist/index.js.map +1 -1
- package/dist/react/index.cjs +81 -140
- package/dist/react/index.cjs.map +1 -1
- package/dist/react/index.d.cts +66 -82
- package/dist/react/index.d.ts +66 -82
- package/dist/react/index.js +82 -136
- package/dist/react/index.js.map +1 -1
- package/dist/{server-ERRDGEZ7.js → server-5N76YCWC.js} +3 -3
- package/dist/{server-ERRDGEZ7.js.map → server-5N76YCWC.js.map} +1 -1
- package/dist/{server-P54IYX2G.cjs → server-QIYIJHD5.cjs} +3 -3
- package/dist/{server-P54IYX2G.cjs.map → server-QIYIJHD5.cjs.map} +1 -1
- package/dist/synapse-runtime.iife.global.js +28 -28
- package/dist/synapse-ui.iife.global.js +2 -2
- package/dist/{types-BSZasM8q.d.cts → types-uEO4VFJ2.d.cts} +87 -166
- package/dist/{types-BSZasM8q.d.ts → types-uEO4VFJ2.d.ts} +87 -166
- package/dist/ui/index.cjs +48 -35
- package/dist/ui/index.cjs.map +1 -1
- package/dist/ui/index.d.cts +1 -1
- package/dist/ui/index.d.ts +1 -1
- package/dist/ui/index.js +48 -35
- package/dist/ui/index.js.map +1 -1
- package/dist/vite/index.cjs +1 -1
- package/dist/vite/index.cjs.map +1 -1
- package/dist/vite/index.js +1 -1
- package/dist/vite/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-GNTQJSQX.js.map +0 -1
- package/dist/chunk-J5ICAHK2.js.map +0 -1
- package/dist/chunk-RYWW6EHB.cjs.map +0 -1
- package/dist/chunk-ZMNYUGSF.cjs.map +0 -1
|
@@ -154,7 +154,6 @@ function applyTheme(mode, hostVars, fontFaces) {
|
|
|
154
154
|
var FONT_FACES_CONTEXT_KEY = "synapse/fontFaces";
|
|
155
155
|
var DEFAULT_THEME = {
|
|
156
156
|
mode: "light",
|
|
157
|
-
primaryColor: "#6366f1",
|
|
158
157
|
tokens: {}
|
|
159
158
|
};
|
|
160
159
|
function detectHost(initResponse) {
|
|
@@ -172,7 +171,7 @@ function extractTheme(ctx) {
|
|
|
172
171
|
const mode = ctx.theme === "light" || ctx.theme === "dark" ? ctx.theme : DEFAULT_THEME.mode;
|
|
173
172
|
const variables = ctx.styles?.variables;
|
|
174
173
|
const tokens = variables && typeof variables === "object" && !Array.isArray(variables) ? variables : {};
|
|
175
|
-
return { mode,
|
|
174
|
+
return { mode, tokens };
|
|
176
175
|
}
|
|
177
176
|
function extractFontFaces(ctx) {
|
|
178
177
|
return normalizeFontFaces(ctx?.[FONT_FACES_CONTEXT_KEY]);
|
|
@@ -181,6 +180,6 @@ function foldFontFaces(prev, ctx) {
|
|
|
181
180
|
return extractFontFaces(ctx) ?? prev;
|
|
182
181
|
}
|
|
183
182
|
|
|
184
|
-
export { FONT_FACES_CONTEXT_KEY, applyTheme, detectHost,
|
|
185
|
-
//# sourceMappingURL=chunk-
|
|
186
|
-
//# sourceMappingURL=chunk-
|
|
183
|
+
export { FONT_FACES_CONTEXT_KEY, applyTheme, detectHost, extractTheme, foldFontFaces, fontFacesKey };
|
|
184
|
+
//# sourceMappingURL=chunk-UPDBDXQB.js.map
|
|
185
|
+
//# sourceMappingURL=chunk-UPDBDXQB.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/theme-defaults.ts","../src/detection.ts"],"names":[],"mappings":"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","file":"chunk-UPDBDXQB.js","sourcesContent":["/**\n * Theme-aware default backing for the `@nimblebrain/synapse/ui` token contract.\n *\n * `tokens` (see `ui/tokens.ts`) are `var(--token, fallback)` references. A CSS\n * `var()` fallback is a *static literal* — it cannot branch on light vs. dark.\n * So any var the host does NOT inject resolves to its single hardcoded (light)\n * fallback in BOTH themes. Pair such a token with a theme-aware one and you get\n * white-on-white in dark mode: it looks correct in light, passes `tsc`/build,\n * and only breaks when the theme is toggled.\n *\n * This module closes that gap with a default layer the SDK controls and that\n * CAN branch on theme. {@link applyThemeVariables} puts the neutral defaults for\n * the active `mode` in a cascade layer and the host's variables inline — so:\n * - the host's (brand) values always win for the keys it provides, through\n * either the protocol or a stylesheet it injects, and\n * - any var nobody declares still resolves to a theme-correct neutral value.\n *\n * It's the `var()` fallback, but able to branch on theme. The values stay\n * neutral grays, a generic blue, and generic semantic hues for danger, success,\n * warning and processing — never a *brand* value. Brand arrives only by host\n * injection, mirroring `ui/tokens.ts`. `__tests__/ui/tokens.test.ts` holds the\n * line: every hex here must be in the sanctioned set. This keeps\n * the library host-agnostic while guaranteeing every token resolves correctly\n * in both themes even against an incomplete host, a standalone `connect()`\n * widget, or a third-party host.\n *\n * Font *faces* are the one part of theming CSS variables cannot carry — a token\n * names a family, it cannot load one. {@link applyThemeFontFaces} closes that\n * half, and {@link applyTheme} applies both together.\n *\n * Only theme-sensitive (color) vars are listed here. Theme-invariant vars\n * (radii, type scale, shadows, font stacks, weights, border widths) look the\n * same in both themes, so their static `var()` fallback is already correct —\n * they are intentionally absent. The `tokens` regression test enforces this\n * with a TOTAL partition: every referenced var must be either declared\n * theme-invariant (an explicit allowlist) or defined in both maps below, so a\n * newly added theme-sensitive token can't slip through unbacked.\n */\n\nimport type { FontDisplayValue, FontFaceDescriptor } from \"./types.js\";\n\nconst FONT_DISPLAY_VALUES = new Set<FontDisplayValue>([\n \"auto\",\n \"block\",\n \"swap\",\n \"fallback\",\n \"optional\",\n]);\n\n/**\n * Light-theme neutral defaults. Values match the (light) fallbacks baked into\n * `tokens` — applying them changes nothing observable in light mode; they exist\n * so the light path flows through the same code as dark and so a standalone\n * render with an explicit `light` theme is backed identically to its fallbacks.\n */\nconst LIGHT: Record<string, string> = {\n // ── Surfaces ──\n \"--color-background-primary\": \"#ffffff\",\n \"--color-background-secondary\": \"#fafafa\",\n \"--color-background-tertiary\": \"#f3f4f6\",\n // ── Text ──\n \"--color-text-primary\": \"#111827\",\n \"--color-text-secondary\": \"#6b7280\",\n \"--color-text-tertiary\": \"#9ca3af\",\n \"--color-text-accent\": \"#2563eb\",\n \"--nb-color-accent-foreground\": \"#ffffff\",\n // ── Border / ring ──\n \"--color-border-primary\": \"#e5e7eb\",\n \"--color-border-secondary\": \"#d1d5db\",\n \"--color-ring-primary\": \"#2563eb\",\n // ── Status / brand semantics ──\n \"--nb-color-danger\": \"#dc2626\",\n \"--nb-color-success\": \"#059669\",\n \"--nb-color-warning\": \"#f59e0b\",\n \"--nb-color-processing\": \"#7c3aed\",\n \"--nb-color-processing-light\": \"#f3eeff\",\n \"--nb-color-info-light\": \"#eef4ff\",\n};\n\n/**\n * Dark-theme neutral defaults. A monotonic neutral ladder — the three surface\n * tiers are the darkest, borders sit a step lighter so they stay visible\n * against every surface, and text inverts to light. \"Subtle\"/\"strong\" keep\n * their light-mode semantics: `bgSubtle` reads as a lifted hover/inset tint and\n * `borderStrong` is more prominent than `border`.\n */\nconst DARK: Record<string, string> = {\n // ── Surfaces (base → lifted) ──\n \"--color-background-primary\": \"#18181b\",\n \"--color-background-secondary\": \"#27272a\",\n \"--color-background-tertiary\": \"#2f2f34\",\n // ── Text ──\n \"--color-text-primary\": \"#fafafa\",\n \"--color-text-secondary\": \"#a1a1aa\",\n \"--color-text-tertiary\": \"#71717a\",\n \"--color-text-accent\": \"#818cf8\",\n \"--nb-color-accent-foreground\": \"#ffffff\",\n // ── Border / ring (lighter than surfaces so they remain visible) ──\n \"--color-border-primary\": \"#3f3f46\",\n \"--color-border-secondary\": \"#52525b\",\n \"--color-ring-primary\": \"#818cf8\",\n // ── Status / brand semantics (brightened for contrast on dark) ──\n \"--nb-color-danger\": \"#f87171\",\n \"--nb-color-success\": \"#34d399\",\n \"--nb-color-warning\": \"#fbbf24\",\n \"--nb-color-processing\": \"#a78bfa\",\n \"--nb-color-processing-light\": \"#2a2440\",\n \"--nb-color-info-light\": \"#1e2a44\",\n};\n\n/**\n * The neutral default theme, keyed by mode. Exported for the regression test\n * that asserts every color var referenced by `tokens` is backed in both modes.\n */\nexport const DEFAULT_THEME_VARS: Record<\"light\" | \"dark\", Record<string, string>> = {\n light: LIGHT,\n dark: DARK,\n};\n\n/** Cascade layer holding the neutral defaults. Named so an app can order it. */\nexport const DEFAULTS_LAYER_NAME = \"synapse-defaults\";\n\n/** `id` of the single `<style>` element this module owns. */\nexport const DEFAULTS_STYLE_ID = \"synapse-theme-defaults\";\n\n/** Custom properties this module last wrote inline, so a later apply clears only\n * what it owns and never an inline property the app set itself. */\nlet appliedInlineKeys = new Set<string>();\n\n/** Test seam — forget what was written inline so a fresh apply is observable. */\nexport function resetAppliedInlineKeys(): void {\n appliedInlineKeys = new Set();\n}\n\n/**\n * Whether this document can host the defaults stylesheet.\n *\n * Not the same question as `typeof document !== \"undefined\"`. An embedder or a\n * test harness may install a *partial* `document` — `documentElement.style` plus\n * listeners and nothing else — which is everything this module needed while the\n * defaults were inline properties. Moving them into a stylesheet added three new\n * requirements (`getElementById`, `createElement`, `head.prepend`), and reaching\n * for them on such a document throws.\n *\n * A throw here is not contained: it unwinds through `applyTheme` into the\n * handshake, so an app in that environment never finishes connecting. The\n * default layer is a rendering nicety and the connection is not, so a document\n * that cannot carry a stylesheet gets no layer rather than no session.\n */\nfunction canInstallStylesheet(): boolean {\n return (\n typeof document !== \"undefined\" &&\n typeof document.getElementById === \"function\" &&\n typeof document.createElement === \"function\" &&\n typeof document.head?.prepend === \"function\"\n );\n}\n\n/**\n * Install the neutral defaults for `mode` as a cascade layer.\n *\n * A layer, not inline properties on `documentElement`, and that distinction is\n * the whole point: **a default has to lose.** An inline style outranks every\n * author stylesheet, so writing defaults there made them beat any rule a host or\n * app had written for the same var — the opposite of a fallback.\n *\n * A host cannot always deliver a var through the protocol.\n * `hostContext.styles.variables` is a closed enum, so a host whose design system\n * is larger than that enum has to put the remainder in a stylesheet it injects\n * into the app document. Those declarations are ordinary unlayered author rules.\n * Unlayered beats layered, so they now win — while a var nobody declares still\n * resolves to a neutral default, which is what this map is for.\n *\n * An app's own `:root` rule wins for the same reason, so overriding a default\n * needs no `!important` and no knowledge of this module.\n *\n * One element, replaced in place, so a mode flip swaps the whole map atomically\n * and repeat calls are cheap. SSR-safe.\n */\nfunction applyDefaultThemeLayer(mode: \"light\" | \"dark\"): void {\n if (!canInstallStylesheet()) return;\n\n // Safe to concatenate ONLY because every key and value here is module-local\n // (`DEFAULT_THEME_VARS`). `applyThemeFontFaces` below deliberately avoids\n // building CSS text for exactly this reason — a host-supplied value containing\n // `}` would escape the rule. Never fold host or app input into this template;\n // host variables go through `setProperty`, which cannot escape.\n const declarations = Object.entries(DEFAULT_THEME_VARS[mode])\n .map(([k, v]) => ` ${k}: ${v};`)\n .join(\"\\n\");\n const css = `@layer ${DEFAULTS_LAYER_NAME} {\\n :root {\\n${declarations}\\n }\\n}`;\n\n const existing = document.getElementById(DEFAULTS_STYLE_ID);\n if (existing) {\n // Avoid a needless style invalidation when the mode hasn't changed.\n if (existing.textContent !== css) existing.textContent = css;\n return;\n }\n const el = document.createElement(\"style\");\n el.id = DEFAULTS_STYLE_ID;\n el.textContent = css;\n // Prepended so this is the first layer the document declares, which puts it\n // first in layer order — an app that declares its own layers sorts after, and\n // therefore wins. Unlayered app rules win regardless of DOM order.\n document.head.prepend(el);\n}\n\n/**\n * Apply theme CSS custom properties to the app document.\n *\n * The host's variables go inline on `documentElement`, where they outrank\n * everything; the neutral defaults for `mode` go into a cascade layer, where\n * they lose to any rule that actually declares the var. So a host value beats\n * this module's default through *either* channel — the protocol or a stylesheet\n * it injects — and a var nobody declares still resolves to a theme-correct\n * default. Only the protocol channel also outranks the app: a host's injected\n * stylesheet and the app's own `:root` are both unlayered rules of equal\n * specificity, so document order decides between them, not this module.\n *\n * A key this module wrote on a previous call and the incoming set does NOT carry\n * is *removed* inline rather than left behind, because a host may legitimately\n * narrow its key set: stop sending a var and it must stop applying, or it stays\n * pinned inline forever where the layer, the host's own stylesheet and the app's\n * own rules all cannot reach it. Removing lets the cascade resolve it against\n * those instead, which is the property this module exists to establish, and is\n * strictly less destructive than overwriting the key with our own default.\n *\n * Tracked as the set of keys last written, not as `DEFAULT_THEME_VARS`'s keys.\n * Those are two different sets: a host can send any spec-enum var, and ~25\n * theme-sensitive ones (`--color-text-danger`, `--color-background-inverse`, …)\n * have no neutral default, so keying the removal off the default map would pin\n * exactly those at the previous mode's value. Tracking what we wrote also means\n * this module never clears an inline property it did not set — an app writing its\n * own `documentElement.style` is left alone.\n *\n * Today an empty var set also arrives for a reason that is a bug rather than a\n * narrowing — `core.ts` replaces the host context wholesale, so a partial\n * `host-context-changed` that omits `styles` reads as \"no tokens\" (#46;\n * `connect.ts` already carries them forward). Fixing #46 is the better outcome —\n * it keeps the host's brand across a mode flip instead of collapsing to our\n * neutral — and does not make this loop unnecessary.\n *\n * SSR-safe (no-ops when `document` is unavailable). Idempotent — re-applying on\n * every theme change is correct and cheap.\n *\n * Prefer {@link applyTheme}: it applies variables *and* font faces together, so\n * a caller cannot wire up half a theme.\n */\nexport function applyThemeVariables(\n mode: \"light\" | \"dark\",\n hostVars: Record<string, string> | undefined | null,\n): void {\n if (typeof document === \"undefined\") return;\n applyDefaultThemeLayer(mode);\n\n // ONE definition of \"the host provided this key\", read by both loops below.\n // Wire data is untyped, so a key can arrive with a non-string value; that is\n // not a provided value, so the clear loop must reach it. Two predicates —\n // `k in hostVars` to clear, `typeof v === \"string\"` to write — leave such a key\n // in the gap: neither removed nor written, so the previous theme's value stays\n // pinned inline where the layer, the host's stylesheet and the app's `:root`\n // all cannot reach it. That is the un-self-healing pin this module exists to\n // eliminate, re-entered by a different door.\n const incoming: Record<string, string> = {};\n if (hostVars && typeof hostVars === \"object\") {\n for (const [k, v] of Object.entries(hostVars)) {\n if (typeof k === \"string\" && typeof v === \"string\") incoming[k] = v;\n }\n }\n\n const root = document.documentElement.style;\n // `removeProperty` is feature-checked and `setProperty` deliberately is not.\n // The setter is the one capability this module has always required, so a\n // document lacking it was never supported and gets no new promise here; the\n // clear pass arrived with the defaults layer, so a `style` carrying only the\n // setter is a shape that used to work. Dropping the clear pass for it is far\n // better than throwing out of the loop and leaving the host's variables\n // unwritten entirely.\n if (typeof root.removeProperty === \"function\") {\n for (const k of appliedInlineKeys) {\n if (!(k in incoming)) root.removeProperty(k);\n }\n }\n for (const [k, v] of Object.entries(incoming)) root.setProperty(k, v);\n appliedInlineKeys = new Set(Object.keys(incoming));\n}\n\n/** The faces this module has added, so a re-apply replaces rather than accumulates. */\nconst managedFaces = new Set<FontFace>();\n\n/** Signature of the currently-applied set — lets a repeat call no-op. */\nlet appliedFontKey = \"\";\n\n/** Stable signature of a face list — the one comparison rule for \"same fonts?\",\n * shared by the sink's re-apply guard and `onThemeChanged`'s equality filter. */\nexport function fontFacesKey(faces: readonly FontFaceDescriptor[] | undefined | null): string {\n if (!Array.isArray(faces)) return \"\";\n return JSON.stringify(faces.map((d) => [d.family, d.src, d.weight, d.style, d.display]));\n}\n\n/**\n * Normalise an untrusted face list into the one shape every layer trusts.\n *\n * THE rule, in one place:\n * - not an array → `undefined` — nothing was said, keep what's loaded\n * - array, none usable → `undefined` — a shape mistake is not a clear\n * - array, some usable → those entries\n * - empty array → `[]` — the explicit clear\n *\n * The wire and the sink each used to spell this out separately and drifted\n * apart, so a host whose descriptors were mis-shaped kept its typeface through\n * one path and lost it through the other. One predicate, one spelling.\n */\nexport function normalizeFontFaces(raw: unknown): FontFaceDescriptor[] | undefined {\n if (!Array.isArray(raw)) return undefined;\n const usable = raw.filter(isFontFaceDescriptor);\n if (raw.length > 0 && usable.length === 0) return undefined;\n return usable;\n}\n\n/**\n * Load host-supplied `@font-face`s into the app document.\n *\n * Uses the CSSOM {@link FontFace} constructor rather than building `@font-face`\n * CSS text. That is deliberate, and load-bearing for two reasons:\n *\n * 1. **No CSS-injection sink.** `family` and `src` are separate arguments, not\n * concatenated into a stylesheet, so a host value containing `}` cannot\n * escape the rule and inject arbitrary CSS into the app.\n * 2. **Uniform URL handling.** The `src` descriptor is parsed by the browser\n * against the `src` grammar, so relative (`url('/fonts/x.woff2')`), absolute\n * (`url('https://cdn.example/x.woff2')`) and `data:` forms all just work.\n * A malformed descriptor throws `SyntaxError` here instead of silently\n * producing dead CSS.\n *\n * The SDK ships no font data. A host that sends nothing leaves the web-safe\n * fallbacks in `ui/tokens.ts` in force — which is why omitting fonts entirely is\n * a supported configuration, not a degraded one.\n *\n * Faces are added but not force-loaded: the browser fetches a face when CSS\n * first matches it, so an unused weight costs nothing. `display` defaults to\n * `swap` so text paints in the fallback immediately rather than blocking.\n *\n * SSR-safe, and safe in environments without the CSS Font Loading API (older\n * jsdom): both no-op. Re-applying an unchanged set is a no-op.\n */\nexport function applyThemeFontFaces(faces: readonly FontFaceDescriptor[] | undefined | null): void {\n // `normalizeFontFaces` owns what absent/garbage means. Reached from the public\n // `applyHostTheme`, so the input here is not necessarily typed: a host\n // building `fontFaces` from untrusted JSON can hand us mis-shaped entries,\n // and reading those as \"clear\" would unload the typeface over a typo.\n const next = normalizeFontFaces(faces);\n if (next === undefined) return;\n if (typeof document === \"undefined\") return;\n // `document.fonts` (CSS Font Loading API) is absent in some test DOMs.\n if (!document.fonts || typeof FontFace === \"undefined\") return;\n\n const key = fontFacesKey(next);\n if (key === appliedFontKey) return;\n appliedFontKey = key;\n\n for (const face of managedFaces) {\n try {\n document.fonts.delete(face);\n } catch {\n // Already evicted, or the API rejected the handle — nothing to undo.\n }\n }\n managedFaces.clear();\n\n for (const d of next) {\n try {\n const face = new FontFace(d.family, d.src, {\n weight: d.weight,\n style: d.style,\n // Wire data is untyped: ignore an unrecognised `display` rather than let\n // the constructor reject the descriptor and cost the app this face.\n display: d.display && FONT_DISPLAY_VALUES.has(d.display) ? d.display : \"swap\",\n });\n document.fonts.add(face);\n managedFaces.add(face);\n } catch {\n // Malformed descriptor — skip this face and keep the web-safe fallback.\n // One bad entry must not cost the app the rest of its typography.\n }\n }\n}\n\nfunction isFontFaceDescriptor(value: unknown): value is FontFaceDescriptor {\n if (!value || typeof value !== \"object\") return false;\n const d = value as Partial<FontFaceDescriptor>;\n return typeof d.family === \"string\" && typeof d.src === \"string\";\n}\n\n/**\n * Apply a resolved theme to the app document — CSS variables and font faces.\n *\n * This is the single path by which theming reaches the DOM: the handshake and\n * `host-context-changed` both funnel through here. Keeping colour and typography on one call is the point — two entry\n * points invite a caller to wire one and forget the other, shipping a host's\n * palette under the wrong typeface.\n *\n * Omitting `fontFaces` is safe and means \"leave the loaded faces alone\" (see\n * {@link applyThemeFontFaces}), so a caller re-applying a theme derived from a\n * partial host context cannot silently strip the host's typeface.\n */\nexport function applyTheme(\n mode: \"light\" | \"dark\",\n hostVars: Record<string, string> | undefined | null,\n fontFaces?: readonly FontFaceDescriptor[] | null,\n): void {\n applyThemeVariables(mode, hostVars);\n applyThemeFontFaces(fontFaces);\n}\n\n/** Test seam — forget applied faces so a fresh apply is observable. */\nexport function resetAppliedFontFaces(): void {\n managedFaces.clear();\n appliedFontKey = \"\";\n}\n","import type { McpUiHostContext, McpUiInitializeResult } from \"@modelcontextprotocol/ext-apps\";\nimport { normalizeFontFaces } from \"./theme-defaults.js\";\nimport type { FontFaceDescriptor, HostInfo, Theme } from \"./types\";\n\n/**\n * Host-context key carrying `@font-face` descriptors.\n *\n * A NimbleBrain extension (`synapse/` prefix), because the ext-apps spec has no\n * equivalent: `styles.variables` is a flat `Record<McpUiStyleVariableKey, string>`\n * that can name a font family but cannot carry the rule that loads it.\n * `McpUiHostContext` declares `[key: string]: unknown` for forward\n * compatibility, so riding an extra key is spec-legal, and hosts that don't send\n * it degrade to the web-safe token fallbacks.\n */\nexport const FONT_FACES_CONTEXT_KEY = \"synapse/fontFaces\";\n\nconst DEFAULT_THEME: Theme = {\n mode: \"light\",\n tokens: {},\n};\n\n/**\n * Detect the host environment from the ext-apps `ui/initialize` response.\n *\n * Reports identity only (host name, protocol version). Theme lives in the\n * unified host-context state and is read via `extractTheme(hostContext)`\n * — no parallel `theme` field on `HostInfo`.\n *\n * Handles missing or malformed fields gracefully — never throws.\n */\nexport function detectHost(initResponse: unknown): HostInfo {\n const resp = initResponse as Partial<McpUiInitializeResult> | null | undefined;\n\n const hostName = resp?.hostInfo?.name ?? \"unknown\";\n const protocolVersion = resp?.protocolVersion ?? \"unknown\";\n\n return {\n isNimbleBrain: hostName === \"nimblebrain\",\n serverName: hostName,\n protocolVersion,\n };\n}\n\nexport function extractTheme(ctx: Partial<McpUiHostContext> | undefined): Theme {\n if (!ctx) return { ...DEFAULT_THEME };\n\n // Spec: theme is a string (\"light\" | \"dark\")\n const mode = ctx.theme === \"light\" || ctx.theme === \"dark\" ? ctx.theme : DEFAULT_THEME.mode;\n\n // Spec: tokens live under styles.variables\n const variables = ctx.styles?.variables;\n const tokens =\n variables && typeof variables === \"object\" && !Array.isArray(variables)\n ? (variables as Record<string, string>)\n : {};\n\n // Deliberately no `fontFaces` here. Faces are sticky across a partial context\n // (see `foldFontFaces`), and this function sees only the context in hand — so\n // populating the field would publish a NON-sticky value on the return type and\n // invite `extractTheme(ctx).fontFaces`, which is the unload-on-toggle bug.\n // `resolveTheme` overlays the sticky set; nothing else should.\n return { mode, tokens };\n}\n\n/**\n * Read the `synapse/fontFaces` host-context extension.\n *\n * `undefined` and `[]` are **distinct answers**, because a host-context-changed\n * carries only the fields that changed:\n * - `undefined` — the host said nothing about fonts, so keep what's loaded.\n * - `[]` — the host explicitly sent an empty list, i.e. drop the faces.\n * Collapsing the two would leave a host no way to clear typography it had\n * previously set.\n *\n * Individual malformed entries are dropped rather than rejecting the batch —\n * one bad face must not cost the app the rest of its typography. Anything that\n * survives is still validated by the browser when the descriptor is built.\n */\nexport function extractFontFaces(\n ctx: Partial<McpUiHostContext> | undefined,\n): FontFaceDescriptor[] | undefined {\n return normalizeFontFaces(ctx?.[FONT_FACES_CONTEXT_KEY]);\n}\n\n/**\n * THE rule for merging incoming faces over what is already loaded, in one\n * place: **absent — or a batch with nothing usable — means unchanged; an\n * explicit list, including empty, replaces.**\n *\n * Every consumer of the host-context font extension folds through here. The\n * rule leaked one layer at a time when each site spelled it itself, so the\n * spelling now lives once and the call sites just call it.\n */\nexport function foldFontFaces(\n prev: FontFaceDescriptor[] | undefined,\n ctx: Partial<McpUiHostContext> | undefined,\n): FontFaceDescriptor[] | undefined {\n return extractFontFaces(ctx) ?? prev;\n}\n"]}
|