@nimblebrain/synapse 0.12.1 → 0.13.0

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.
Files changed (55) hide show
  1. package/README.md +58 -4
  2. package/dist/{chunk-JVKQNMAP.js → chunk-5AR3IROW.js} +11 -6
  3. package/dist/chunk-5AR3IROW.js.map +1 -0
  4. package/dist/{chunk-XGJAADVV.js → chunk-AGN3NDOK.js} +25 -36
  5. package/dist/chunk-AGN3NDOK.js.map +1 -0
  6. package/dist/chunk-DOKVNVTI.js +162 -0
  7. package/dist/chunk-DOKVNVTI.js.map +1 -0
  8. package/dist/chunk-IA2M4GTK.cjs +170 -0
  9. package/dist/chunk-IA2M4GTK.cjs.map +1 -0
  10. package/dist/{chunk-A4ENX6XW.cjs → chunk-QHS7BJLI.cjs} +28 -39
  11. package/dist/chunk-QHS7BJLI.cjs.map +1 -0
  12. package/dist/{chunk-ANUZ7WFQ.cjs → chunk-VMUZMVKW.cjs} +11 -6
  13. package/dist/chunk-VMUZMVKW.cjs.map +1 -0
  14. package/dist/codegen/index.d.cts +1 -1
  15. package/dist/codegen/index.d.ts +1 -1
  16. package/dist/connect.iife.global.js +25 -25
  17. package/dist/{detect-DSnLT-_m.d.ts → detect-C_qmYsh9.d.cts} +6 -0
  18. package/dist/{detect-DSnLT-_m.d.cts → detect-Dfd0upoz.d.ts} +6 -0
  19. package/dist/host/index.cjs +14 -14
  20. package/dist/host/index.d.cts +10 -3
  21. package/dist/host/index.d.ts +10 -3
  22. package/dist/host/index.js +2 -2
  23. package/dist/index.cjs +13 -9
  24. package/dist/index.d.cts +16 -4
  25. package/dist/index.d.ts +16 -4
  26. package/dist/index.js +3 -3
  27. package/dist/react/index.cjs +5 -5
  28. package/dist/react/index.cjs.map +1 -1
  29. package/dist/react/index.d.cts +1 -1
  30. package/dist/react/index.d.ts +1 -1
  31. package/dist/react/index.js +3 -3
  32. package/dist/react/index.js.map +1 -1
  33. package/dist/synapse-runtime.iife.global.js +25 -25
  34. package/dist/synapse-ui.iife.global.js +1 -1
  35. package/dist/{types-C9utGXv_.d.cts → types-BSZasM8q.d.cts} +39 -1
  36. package/dist/{types-C9utGXv_.d.ts → types-BSZasM8q.d.ts} +39 -1
  37. package/dist/ui/index.cjs +34 -1
  38. package/dist/ui/index.cjs.map +1 -1
  39. package/dist/ui/index.js +34 -1
  40. package/dist/ui/index.js.map +1 -1
  41. package/package.json +1 -6
  42. package/dist/chunk-A4ENX6XW.cjs.map +0 -1
  43. package/dist/chunk-ANUZ7WFQ.cjs.map +0 -1
  44. package/dist/chunk-JVKQNMAP.js.map +0 -1
  45. package/dist/chunk-KTXASIFJ.cjs +0 -75
  46. package/dist/chunk-KTXASIFJ.cjs.map +0 -1
  47. package/dist/chunk-VXTMI266.js +0 -73
  48. package/dist/chunk-VXTMI266.js.map +0 -1
  49. package/dist/chunk-XGJAADVV.js.map +0 -1
  50. package/dist/ui/fonts.cjs +0 -19
  51. package/dist/ui/fonts.cjs.map +0 -1
  52. package/dist/ui/fonts.d.cts +0 -30
  53. package/dist/ui/fonts.d.ts +0 -30
  54. package/dist/ui/fonts.js +0 -17
  55. package/dist/ui/fonts.js.map +0 -1
@@ -0,0 +1,162 @@
1
+ // src/theme-defaults.ts
2
+ var FONT_DISPLAY_VALUES = /* @__PURE__ */ new Set([
3
+ "auto",
4
+ "block",
5
+ "swap",
6
+ "fallback",
7
+ "optional"
8
+ ]);
9
+ var LIGHT = {
10
+ // ── Surfaces ──
11
+ "--color-background-primary": "#ffffff",
12
+ "--color-background-secondary": "#fafafa",
13
+ "--color-background-tertiary": "#f3f4f6",
14
+ // ── Text ──
15
+ "--color-text-primary": "#111827",
16
+ "--color-text-secondary": "#6b7280",
17
+ "--color-text-tertiary": "#9ca3af",
18
+ "--color-text-accent": "#2563eb",
19
+ "--nb-color-accent-foreground": "#ffffff",
20
+ // ── Border / ring ──
21
+ "--color-border-primary": "#e5e7eb",
22
+ "--color-border-secondary": "#d1d5db",
23
+ "--color-ring-primary": "#2563eb",
24
+ // ── Status / brand semantics ──
25
+ "--nb-color-danger": "#dc2626",
26
+ "--nb-color-success": "#059669",
27
+ "--nb-color-warning": "#f59e0b",
28
+ "--nb-color-warm": "#d4620a",
29
+ "--nb-color-warm-light": "#fef5ee",
30
+ "--nb-color-processing": "#7c3aed",
31
+ "--nb-color-processing-light": "#f3eeff",
32
+ "--nb-color-info-light": "#eef4ff"
33
+ };
34
+ var DARK = {
35
+ // ── Surfaces (base → lifted) ──
36
+ "--color-background-primary": "#18181b",
37
+ "--color-background-secondary": "#27272a",
38
+ "--color-background-tertiary": "#2f2f34",
39
+ // ── Text ──
40
+ "--color-text-primary": "#fafafa",
41
+ "--color-text-secondary": "#a1a1aa",
42
+ "--color-text-tertiary": "#71717a",
43
+ "--color-text-accent": "#818cf8",
44
+ "--nb-color-accent-foreground": "#ffffff",
45
+ // ── Border / ring (lighter than surfaces so they remain visible) ──
46
+ "--color-border-primary": "#3f3f46",
47
+ "--color-border-secondary": "#52525b",
48
+ "--color-ring-primary": "#818cf8",
49
+ // ── Status / brand semantics (brightened for contrast on dark) ──
50
+ "--nb-color-danger": "#f87171",
51
+ "--nb-color-success": "#34d399",
52
+ "--nb-color-warning": "#fbbf24",
53
+ "--nb-color-warm": "#fb923c",
54
+ "--nb-color-warm-light": "#3a2a1e",
55
+ "--nb-color-processing": "#a78bfa",
56
+ "--nb-color-processing-light": "#2a2440",
57
+ "--nb-color-info-light": "#1e2a44"
58
+ };
59
+ var DEFAULT_THEME_VARS = {
60
+ light: LIGHT,
61
+ dark: DARK
62
+ };
63
+ function applyThemeVariables(mode, hostVars) {
64
+ if (typeof document === "undefined") return;
65
+ const root = document.documentElement.style;
66
+ for (const [k, v] of Object.entries(DEFAULT_THEME_VARS[mode])) {
67
+ root.setProperty(k, v);
68
+ }
69
+ if (hostVars && typeof hostVars === "object") {
70
+ for (const [k, v] of Object.entries(hostVars)) {
71
+ if (typeof k === "string" && typeof v === "string") {
72
+ root.setProperty(k, v);
73
+ }
74
+ }
75
+ }
76
+ }
77
+ var managedFaces = /* @__PURE__ */ new Set();
78
+ var appliedFontKey = "";
79
+ function fontFacesKey(faces) {
80
+ if (!Array.isArray(faces)) return "";
81
+ return JSON.stringify(faces.map((d) => [d.family, d.src, d.weight, d.style, d.display]));
82
+ }
83
+ function normalizeFontFaces(raw) {
84
+ if (!Array.isArray(raw)) return void 0;
85
+ const usable = raw.filter(isFontFaceDescriptor);
86
+ if (raw.length > 0 && usable.length === 0) return void 0;
87
+ return usable;
88
+ }
89
+ function applyThemeFontFaces(faces) {
90
+ const next = normalizeFontFaces(faces);
91
+ if (next === void 0) return;
92
+ if (typeof document === "undefined") return;
93
+ if (!document.fonts || typeof FontFace === "undefined") return;
94
+ const key = fontFacesKey(next);
95
+ if (key === appliedFontKey) return;
96
+ appliedFontKey = key;
97
+ for (const face of managedFaces) {
98
+ try {
99
+ document.fonts.delete(face);
100
+ } catch {
101
+ }
102
+ }
103
+ managedFaces.clear();
104
+ for (const d of next) {
105
+ try {
106
+ const face = new FontFace(d.family, d.src, {
107
+ weight: d.weight,
108
+ style: d.style,
109
+ // Wire data is untyped: ignore an unrecognised `display` rather than let
110
+ // the constructor reject the descriptor and cost the app this face.
111
+ display: d.display && FONT_DISPLAY_VALUES.has(d.display) ? d.display : "swap"
112
+ });
113
+ document.fonts.add(face);
114
+ managedFaces.add(face);
115
+ } catch {
116
+ }
117
+ }
118
+ }
119
+ function isFontFaceDescriptor(value) {
120
+ if (!value || typeof value !== "object") return false;
121
+ const d = value;
122
+ return typeof d.family === "string" && typeof d.src === "string";
123
+ }
124
+ function applyTheme(mode, hostVars, fontFaces) {
125
+ applyThemeVariables(mode, hostVars);
126
+ applyThemeFontFaces(fontFaces);
127
+ }
128
+
129
+ // src/detection.ts
130
+ var FONT_FACES_CONTEXT_KEY = "synapse/fontFaces";
131
+ var DEFAULT_THEME = {
132
+ mode: "light",
133
+ primaryColor: "#6366f1",
134
+ tokens: {}
135
+ };
136
+ function detectHost(initResponse) {
137
+ const resp = initResponse;
138
+ const hostName = resp?.hostInfo?.name ?? "unknown";
139
+ const protocolVersion = resp?.protocolVersion ?? "unknown";
140
+ return {
141
+ isNimbleBrain: hostName === "nimblebrain",
142
+ serverName: hostName,
143
+ protocolVersion
144
+ };
145
+ }
146
+ function extractTheme(ctx) {
147
+ if (!ctx) return { ...DEFAULT_THEME };
148
+ const mode = ctx.theme === "light" || ctx.theme === "dark" ? ctx.theme : DEFAULT_THEME.mode;
149
+ const variables = ctx.styles?.variables;
150
+ const tokens = variables && typeof variables === "object" && !Array.isArray(variables) ? variables : {};
151
+ return { mode, primaryColor: DEFAULT_THEME.primaryColor, tokens };
152
+ }
153
+ function extractFontFaces(ctx) {
154
+ return normalizeFontFaces(ctx?.[FONT_FACES_CONTEXT_KEY]);
155
+ }
156
+ function foldFontFaces(prev, ctx) {
157
+ return extractFontFaces(ctx) ?? prev;
158
+ }
159
+
160
+ export { FONT_FACES_CONTEXT_KEY, applyTheme, detectHost, extractFontFaces, extractTheme, foldFontFaces, fontFacesKey };
161
+ //# sourceMappingURL=chunk-DOKVNVTI.js.map
162
+ //# sourceMappingURL=chunk-DOKVNVTI.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/theme-defaults.ts","../src/detection.ts"],"names":[],"mappings":"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","file":"chunk-DOKVNVTI.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} writes the neutral defaults\n * for the active `mode` first, then the host's variables on top — so:\n * - the host's (brand) values always win for the keys it provides, and\n * - any var the host omits 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 * deliberately NEUTRAL (grays + a generic blue), never NimbleBrain brand —\n * brand arrives only by host injection, mirroring `ui/tokens.ts`. 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-warm\": \"#d4620a\",\n \"--nb-color-warm-light\": \"#fef5ee\",\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-warm\": \"#fb923c\",\n \"--nb-color-warm-light\": \"#3a2a1e\",\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/**\n * Apply theme CSS custom properties onto `document.documentElement`.\n *\n * Writes the neutral defaults for `mode` FIRST, then the host's variables on\n * top — so the host's values win for the keys it provides, and any var it omits\n * still resolves to a theme-correct neutral default.\n *\n * SSR-safe (no-ops when `document` is unavailable). Idempotent — `setProperty`\n * overwrites, so re-applying on 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 const root = document.documentElement.style;\n for (const [k, v] of Object.entries(DEFAULT_THEME_VARS[mode])) {\n root.setProperty(k, v);\n }\n if (hostVars && typeof hostVars === \"object\") {\n for (const [k, v] of Object.entries(hostVars)) {\n if (typeof k === \"string\" && typeof v === \"string\") {\n root.setProperty(k, v);\n }\n }\n }\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,\n * `host-context-changed`, and the React `<SynapseProvider>` all funnel through\n * 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, SynapseTheme } 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: SynapseTheme = {\n mode: \"light\",\n primaryColor: \"#6366f1\",\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): SynapseTheme {\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, primaryColor: DEFAULT_THEME.primaryColor, 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"]}
@@ -0,0 +1,170 @@
1
+ 'use strict';
2
+
3
+ // src/theme-defaults.ts
4
+ var FONT_DISPLAY_VALUES = /* @__PURE__ */ new Set([
5
+ "auto",
6
+ "block",
7
+ "swap",
8
+ "fallback",
9
+ "optional"
10
+ ]);
11
+ var LIGHT = {
12
+ // ── Surfaces ──
13
+ "--color-background-primary": "#ffffff",
14
+ "--color-background-secondary": "#fafafa",
15
+ "--color-background-tertiary": "#f3f4f6",
16
+ // ── Text ──
17
+ "--color-text-primary": "#111827",
18
+ "--color-text-secondary": "#6b7280",
19
+ "--color-text-tertiary": "#9ca3af",
20
+ "--color-text-accent": "#2563eb",
21
+ "--nb-color-accent-foreground": "#ffffff",
22
+ // ── Border / ring ──
23
+ "--color-border-primary": "#e5e7eb",
24
+ "--color-border-secondary": "#d1d5db",
25
+ "--color-ring-primary": "#2563eb",
26
+ // ── Status / brand semantics ──
27
+ "--nb-color-danger": "#dc2626",
28
+ "--nb-color-success": "#059669",
29
+ "--nb-color-warning": "#f59e0b",
30
+ "--nb-color-warm": "#d4620a",
31
+ "--nb-color-warm-light": "#fef5ee",
32
+ "--nb-color-processing": "#7c3aed",
33
+ "--nb-color-processing-light": "#f3eeff",
34
+ "--nb-color-info-light": "#eef4ff"
35
+ };
36
+ var DARK = {
37
+ // ── Surfaces (base → lifted) ──
38
+ "--color-background-primary": "#18181b",
39
+ "--color-background-secondary": "#27272a",
40
+ "--color-background-tertiary": "#2f2f34",
41
+ // ── Text ──
42
+ "--color-text-primary": "#fafafa",
43
+ "--color-text-secondary": "#a1a1aa",
44
+ "--color-text-tertiary": "#71717a",
45
+ "--color-text-accent": "#818cf8",
46
+ "--nb-color-accent-foreground": "#ffffff",
47
+ // ── Border / ring (lighter than surfaces so they remain visible) ──
48
+ "--color-border-primary": "#3f3f46",
49
+ "--color-border-secondary": "#52525b",
50
+ "--color-ring-primary": "#818cf8",
51
+ // ── Status / brand semantics (brightened for contrast on dark) ──
52
+ "--nb-color-danger": "#f87171",
53
+ "--nb-color-success": "#34d399",
54
+ "--nb-color-warning": "#fbbf24",
55
+ "--nb-color-warm": "#fb923c",
56
+ "--nb-color-warm-light": "#3a2a1e",
57
+ "--nb-color-processing": "#a78bfa",
58
+ "--nb-color-processing-light": "#2a2440",
59
+ "--nb-color-info-light": "#1e2a44"
60
+ };
61
+ var DEFAULT_THEME_VARS = {
62
+ light: LIGHT,
63
+ dark: DARK
64
+ };
65
+ function applyThemeVariables(mode, hostVars) {
66
+ if (typeof document === "undefined") return;
67
+ const root = document.documentElement.style;
68
+ for (const [k, v] of Object.entries(DEFAULT_THEME_VARS[mode])) {
69
+ root.setProperty(k, v);
70
+ }
71
+ if (hostVars && typeof hostVars === "object") {
72
+ for (const [k, v] of Object.entries(hostVars)) {
73
+ if (typeof k === "string" && typeof v === "string") {
74
+ root.setProperty(k, v);
75
+ }
76
+ }
77
+ }
78
+ }
79
+ var managedFaces = /* @__PURE__ */ new Set();
80
+ var appliedFontKey = "";
81
+ function fontFacesKey(faces) {
82
+ if (!Array.isArray(faces)) return "";
83
+ return JSON.stringify(faces.map((d) => [d.family, d.src, d.weight, d.style, d.display]));
84
+ }
85
+ function normalizeFontFaces(raw) {
86
+ if (!Array.isArray(raw)) return void 0;
87
+ const usable = raw.filter(isFontFaceDescriptor);
88
+ if (raw.length > 0 && usable.length === 0) return void 0;
89
+ return usable;
90
+ }
91
+ function applyThemeFontFaces(faces) {
92
+ const next = normalizeFontFaces(faces);
93
+ if (next === void 0) return;
94
+ if (typeof document === "undefined") return;
95
+ if (!document.fonts || typeof FontFace === "undefined") return;
96
+ const key = fontFacesKey(next);
97
+ if (key === appliedFontKey) return;
98
+ appliedFontKey = key;
99
+ for (const face of managedFaces) {
100
+ try {
101
+ document.fonts.delete(face);
102
+ } catch {
103
+ }
104
+ }
105
+ managedFaces.clear();
106
+ for (const d of next) {
107
+ try {
108
+ const face = new FontFace(d.family, d.src, {
109
+ weight: d.weight,
110
+ style: d.style,
111
+ // Wire data is untyped: ignore an unrecognised `display` rather than let
112
+ // the constructor reject the descriptor and cost the app this face.
113
+ display: d.display && FONT_DISPLAY_VALUES.has(d.display) ? d.display : "swap"
114
+ });
115
+ document.fonts.add(face);
116
+ managedFaces.add(face);
117
+ } catch {
118
+ }
119
+ }
120
+ }
121
+ function isFontFaceDescriptor(value) {
122
+ if (!value || typeof value !== "object") return false;
123
+ const d = value;
124
+ return typeof d.family === "string" && typeof d.src === "string";
125
+ }
126
+ function applyTheme(mode, hostVars, fontFaces) {
127
+ applyThemeVariables(mode, hostVars);
128
+ applyThemeFontFaces(fontFaces);
129
+ }
130
+
131
+ // src/detection.ts
132
+ var FONT_FACES_CONTEXT_KEY = "synapse/fontFaces";
133
+ var DEFAULT_THEME = {
134
+ mode: "light",
135
+ primaryColor: "#6366f1",
136
+ tokens: {}
137
+ };
138
+ function detectHost(initResponse) {
139
+ const resp = initResponse;
140
+ const hostName = resp?.hostInfo?.name ?? "unknown";
141
+ const protocolVersion = resp?.protocolVersion ?? "unknown";
142
+ return {
143
+ isNimbleBrain: hostName === "nimblebrain",
144
+ serverName: hostName,
145
+ protocolVersion
146
+ };
147
+ }
148
+ function extractTheme(ctx) {
149
+ if (!ctx) return { ...DEFAULT_THEME };
150
+ const mode = ctx.theme === "light" || ctx.theme === "dark" ? ctx.theme : DEFAULT_THEME.mode;
151
+ const variables = ctx.styles?.variables;
152
+ const tokens = variables && typeof variables === "object" && !Array.isArray(variables) ? variables : {};
153
+ return { mode, primaryColor: DEFAULT_THEME.primaryColor, tokens };
154
+ }
155
+ function extractFontFaces(ctx) {
156
+ return normalizeFontFaces(ctx?.[FONT_FACES_CONTEXT_KEY]);
157
+ }
158
+ function foldFontFaces(prev, ctx) {
159
+ return extractFontFaces(ctx) ?? prev;
160
+ }
161
+
162
+ exports.FONT_FACES_CONTEXT_KEY = FONT_FACES_CONTEXT_KEY;
163
+ exports.applyTheme = applyTheme;
164
+ exports.detectHost = detectHost;
165
+ exports.extractFontFaces = extractFontFaces;
166
+ exports.extractTheme = extractTheme;
167
+ exports.foldFontFaces = foldFontFaces;
168
+ exports.fontFacesKey = fontFacesKey;
169
+ //# sourceMappingURL=chunk-IA2M4GTK.cjs.map
170
+ //# sourceMappingURL=chunk-IA2M4GTK.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/theme-defaults.ts","../src/detection.ts"],"names":[],"mappings":"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","file":"chunk-IA2M4GTK.cjs","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} writes the neutral defaults\n * for the active `mode` first, then the host's variables on top — so:\n * - the host's (brand) values always win for the keys it provides, and\n * - any var the host omits 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 * deliberately NEUTRAL (grays + a generic blue), never NimbleBrain brand —\n * brand arrives only by host injection, mirroring `ui/tokens.ts`. 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-warm\": \"#d4620a\",\n \"--nb-color-warm-light\": \"#fef5ee\",\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-warm\": \"#fb923c\",\n \"--nb-color-warm-light\": \"#3a2a1e\",\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/**\n * Apply theme CSS custom properties onto `document.documentElement`.\n *\n * Writes the neutral defaults for `mode` FIRST, then the host's variables on\n * top — so the host's values win for the keys it provides, and any var it omits\n * still resolves to a theme-correct neutral default.\n *\n * SSR-safe (no-ops when `document` is unavailable). Idempotent — `setProperty`\n * overwrites, so re-applying on 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 const root = document.documentElement.style;\n for (const [k, v] of Object.entries(DEFAULT_THEME_VARS[mode])) {\n root.setProperty(k, v);\n }\n if (hostVars && typeof hostVars === \"object\") {\n for (const [k, v] of Object.entries(hostVars)) {\n if (typeof k === \"string\" && typeof v === \"string\") {\n root.setProperty(k, v);\n }\n }\n }\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,\n * `host-context-changed`, and the React `<SynapseProvider>` all funnel through\n * 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, SynapseTheme } 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: SynapseTheme = {\n mode: \"light\",\n primaryColor: \"#6366f1\",\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): SynapseTheme {\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, primaryColor: DEFAULT_THEME.primaryColor, 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"]}
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- var chunkKTXASIFJ_cjs = require('./chunk-KTXASIFJ.cjs');
3
+ var chunkIA2M4GTK_cjs = require('./chunk-IA2M4GTK.cjs');
4
4
  var extApps = require('@modelcontextprotocol/ext-apps');
5
5
 
6
6
  // src/content-parser.ts
@@ -254,9 +254,11 @@ async function connect(options) {
254
254
  };
255
255
  const ctx = result.hostContext;
256
256
  if (ctx) {
257
+ const initialFontFaces = chunkIA2M4GTK_cjs.extractFontFaces(ctx);
257
258
  currentTheme = {
258
259
  mode: ctx.theme === "dark" ? "dark" : "light",
259
- tokens: ctx.styles?.variables && typeof ctx.styles.variables === "object" ? ctx.styles.variables : {}
260
+ tokens: ctx.styles?.variables && typeof ctx.styles.variables === "object" ? ctx.styles.variables : {},
261
+ ...initialFontFaces ? { fontFaces: initialFontFaces } : {}
260
262
  };
261
263
  if (ctx.toolInfo && typeof ctx.toolInfo === "object") {
262
264
  toolInfo = { tool: ctx.toolInfo.tool ?? {} };
@@ -264,7 +266,7 @@ async function connect(options) {
264
266
  if (ctx.containerDimensions && typeof ctx.containerDimensions === "object") {
265
267
  containerDimensions = ctx.containerDimensions;
266
268
  }
267
- chunkKTXASIFJ_cjs.applyThemeVariables(currentTheme.mode, currentTheme.tokens);
269
+ chunkIA2M4GTK_cjs.applyTheme(currentTheme.mode, currentTheme.tokens, currentTheme.fontFaces);
268
270
  }
269
271
  }
270
272
  transport.onMessage(extApps.HOST_CONTEXT_CHANGED_METHOD, (params) => {
@@ -273,8 +275,9 @@ async function connect(options) {
273
275
  const mode = ctx.theme === "dark" ? "dark" : "light";
274
276
  const variables = ctx.styles?.variables;
275
277
  const tokens = variables && typeof variables === "object" ? variables : currentTheme.tokens;
276
- currentTheme = { mode, tokens };
277
- chunkKTXASIFJ_cjs.applyThemeVariables(mode, tokens);
278
+ const fontFaces = chunkIA2M4GTK_cjs.foldFontFaces(currentTheme.fontFaces, ctx);
279
+ currentTheme = { mode, tokens, ...fontFaces ? { fontFaces } : {} };
280
+ chunkIA2M4GTK_cjs.applyTheme(mode, tokens, fontFaces);
278
281
  const set = handlers.get(extApps.HOST_CONTEXT_CHANGED_METHOD);
279
282
  if (set) {
280
283
  for (const handler of set) handler(currentTheme);
@@ -398,30 +401,6 @@ async function connect(options) {
398
401
  return app;
399
402
  }
400
403
 
401
- // src/detection.ts
402
- var DEFAULT_THEME = {
403
- mode: "light",
404
- primaryColor: "#6366f1",
405
- tokens: {}
406
- };
407
- function detectHost(initResponse) {
408
- const resp = initResponse;
409
- const hostName = resp?.hostInfo?.name ?? "unknown";
410
- const protocolVersion = resp?.protocolVersion ?? "unknown";
411
- return {
412
- isNimbleBrain: hostName === "nimblebrain",
413
- serverName: hostName,
414
- protocolVersion
415
- };
416
- }
417
- function extractTheme(ctx) {
418
- if (!ctx) return { ...DEFAULT_THEME };
419
- const mode = ctx.theme === "light" || ctx.theme === "dark" ? ctx.theme : DEFAULT_THEME.mode;
420
- const variables = ctx.styles?.variables;
421
- const tokens = variables && typeof variables === "object" && !Array.isArray(variables) ? variables : {};
422
- return { mode, primaryColor: DEFAULT_THEME.primaryColor, tokens };
423
- }
424
-
425
404
  // src/keyboard.ts
426
405
  var KeyboardForwarder = class {
427
406
  listener;
@@ -610,6 +589,15 @@ function createSynapse(options) {
610
589
  const transport = new SynapseTransport();
611
590
  let hostInfo = null;
612
591
  let currentHostContext = {};
592
+ let currentFontFaces;
593
+ function syncFontFaces(ctx) {
594
+ currentFontFaces = chunkIA2M4GTK_cjs.foldFontFaces(currentFontFaces, ctx);
595
+ return currentFontFaces;
596
+ }
597
+ function resolveTheme(ctx) {
598
+ const theme = chunkIA2M4GTK_cjs.extractTheme(ctx);
599
+ return currentFontFaces ? { ...theme, fontFaces: currentFontFaces } : theme;
600
+ }
613
601
  let hostTasksCapability = null;
614
602
  let destroyed = false;
615
603
  const taskStatusRouter = createTaskStatusRouter(transport);
@@ -627,14 +615,14 @@ function createSynapse(options) {
627
615
  appCapabilities
628
616
  };
629
617
  const ready = transport.request(extApps.INITIALIZE_METHOD, initParams).then((result) => {
630
- hostInfo = detectHost(result);
618
+ hostInfo = chunkIA2M4GTK_cjs.detectHost(result);
631
619
  currentHostContext = result?.hostContext ?? {};
632
620
  const initResult = result;
633
621
  const rawTasks = initResult?.hostCapabilities?.tasks;
634
622
  hostTasksCapability = rawTasks && typeof rawTasks === "object" && !Array.isArray(rawTasks) ? rawTasks : void 0;
635
623
  {
636
- const theme = extractTheme(currentHostContext);
637
- chunkKTXASIFJ_cjs.applyThemeVariables(theme.mode, theme.tokens);
624
+ const theme = chunkIA2M4GTK_cjs.extractTheme(currentHostContext);
625
+ chunkIA2M4GTK_cjs.applyTheme(theme.mode, theme.tokens, syncFontFaces(currentHostContext));
638
626
  }
639
627
  for (const cb of hostContextCallbacks) cb(currentHostContext);
640
628
  transport.send(extApps.INITIALIZED_METHOD, {});
@@ -642,8 +630,8 @@ function createSynapse(options) {
642
630
  });
643
631
  const unsubHostContext = transport.onMessage(extApps.HOST_CONTEXT_CHANGED_METHOD, (params) => {
644
632
  currentHostContext = params ?? {};
645
- const theme = extractTheme(currentHostContext);
646
- chunkKTXASIFJ_cjs.applyThemeVariables(theme.mode, theme.tokens);
633
+ const theme = chunkIA2M4GTK_cjs.extractTheme(currentHostContext);
634
+ chunkIA2M4GTK_cjs.applyTheme(theme.mode, theme.tokens, syncFontFaces(currentHostContext));
647
635
  for (const cb of hostContextCallbacks) cb(currentHostContext);
648
636
  });
649
637
  const hostContextCallbacks = /* @__PURE__ */ new Set();
@@ -734,7 +722,7 @@ function createSynapse(options) {
734
722
  };
735
723
  },
736
724
  getTheme() {
737
- return extractTheme(currentHostContext);
725
+ return resolveTheme(currentHostContext);
738
726
  },
739
727
  // Selector over `onHostContextChanged`: only fires when the *derived*
740
728
  // theme actually changes, so theme subscribers don't see spurious
@@ -750,9 +738,9 @@ function createSynapse(options) {
750
738
  // current derived theme, so a workspace-only `host-context-changed`
751
739
  // notification correctly filters as a no-op.
752
740
  onThemeChanged(callback) {
753
- let prev = hostInfo !== null ? extractTheme(currentHostContext) : null;
741
+ let prev = hostInfo !== null ? resolveTheme(currentHostContext) : null;
754
742
  const wrapped = (ctx) => {
755
- const next = extractTheme(ctx);
743
+ const next = resolveTheme(ctx);
756
744
  if (prev !== null && themesEqual(prev, next)) return;
757
745
  prev = next;
758
746
  callback(next);
@@ -870,6 +858,7 @@ function validateFileResult(value) {
870
858
  }
871
859
  function themesEqual(a, b) {
872
860
  if (a.mode !== b.mode || a.primaryColor !== b.primaryColor) return false;
861
+ if (chunkIA2M4GTK_cjs.fontFacesKey(a.fontFaces) !== chunkIA2M4GTK_cjs.fontFacesKey(b.fontFaces)) return false;
873
862
  const aKeys = Object.keys(a.tokens);
874
863
  const bKeys = Object.keys(b.tokens);
875
864
  if (aKeys.length !== bKeys.length) return false;
@@ -881,5 +870,5 @@ function themesEqual(a, b) {
881
870
 
882
871
  exports.connect = connect;
883
872
  exports.createSynapse = createSynapse;
884
- //# sourceMappingURL=chunk-A4ENX6XW.cjs.map
885
- //# sourceMappingURL=chunk-A4ENX6XW.cjs.map
873
+ //# sourceMappingURL=chunk-QHS7BJLI.cjs.map
874
+ //# sourceMappingURL=chunk-QHS7BJLI.cjs.map