@spectare-personalisation/react 0.2.1 → 0.4.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.
package/dist/index.js CHANGED
@@ -7,6 +7,7 @@ import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
7
7
 
8
8
  // src/types.ts
9
9
  var COMPONENT_ZONES = {
10
+ AnnouncementBar: "announce",
10
11
  HeroStatement: "hero",
11
12
  ImageCtaHero: "hero",
12
13
  AtomCard: "body",
@@ -24,23 +25,102 @@ function filterAssemblyByZone(assembly, zone) {
24
25
  return effectiveZone === zone;
25
26
  });
26
27
  }
28
+
29
+ // src/tokens.ts
30
+ var DEFAULT_ACCENT = "#60a5fa";
31
+ var SP_ROOT = "sp-root";
32
+ function palette(accent) {
33
+ return {
34
+ "--sp-accent": accent,
35
+ "--sp-accent-bg": `color-mix(in srgb,${accent} 10%,transparent)`,
36
+ "--sp-accent-border": `color-mix(in srgb,${accent} 20%,transparent)`,
37
+ "--sp-bg": "#18181b",
38
+ "--sp-surface": "#09090b",
39
+ "--sp-border": "#27272a",
40
+ "--sp-text": "#f4f4f5",
41
+ "--sp-text-sub": "#d4d4d8",
42
+ "--sp-text-strong": "#e4e4e7",
43
+ "--sp-text-muted": "#82828b",
44
+ "--sp-text-dim": "#6b6b74",
45
+ "--sp-text-body": "#a1a1aa",
46
+ "--sp-positive": "#6ee7b7",
47
+ "--sp-cta-bg": "#fff",
48
+ "--sp-cta-text": "#09090b",
49
+ "--sp-divider": "rgba(255,255,255,0.06)",
50
+ "--sp-code-bg": "rgba(255,255,255,0.07)",
51
+ "--sp-code-border": "rgba(255,255,255,0.08)",
52
+ "--sp-pre-bg": "rgba(0,0,0,0.4)"
53
+ };
54
+ }
55
+ function rules(unit) {
56
+ const gap = unit === "px" ? "20px" : "1.25rem";
57
+ return [
58
+ ".sp-root .sp-prose p{margin-bottom:.625rem}",
59
+ ".sp-root .sp-prose p:last-child{margin-bottom:0}",
60
+ ".sp-root .sp-prose ul{list-style:disc;padding-left:1.25rem;margin-bottom:.625rem}",
61
+ ".sp-root .sp-prose ol{list-style:decimal;padding-left:1.25rem;margin-bottom:.625rem}",
62
+ ".sp-root .sp-prose li{margin-bottom:.25rem}",
63
+ ".sp-root .sp-prose strong{color:var(--sp-text-strong);font-weight:600}",
64
+ ".sp-root .sp-prose h2,.sp-root .sp-prose h3{color:var(--sp-text);font-weight:600;margin:.875rem 0 .375rem;font-size:1rem}",
65
+ ".sp-root .sp-prose code{background:var(--sp-code-bg);border:1px solid var(--sp-code-border);padding:.1rem .35rem;border-radius:.25rem;font-size:.82em;color:var(--sp-text)}",
66
+ ".sp-root .sp-prose blockquote{border-left:3px solid var(--sp-border);padding-left:1rem;color:var(--sp-text-muted);margin:.75rem 0}",
67
+ `.sp-root .sp-pair{display:grid;grid-template-columns:1fr 1fr;gap:${gap}}`,
68
+ "@media(max-width:640px){.sp-root .sp-pair{grid-template-columns:1fr}}"
69
+ ].join("");
70
+ }
71
+ function buildTokenCss({ accent = DEFAULT_ACCENT, unit = "px", scope = `.${SP_ROOT}` } = {}) {
72
+ const decls = Object.entries(palette(accent)).map(([k, v]) => `${k}:${v}`).join(";");
73
+ return `${scope}{${decls}}${rules(unit)}`;
74
+ }
27
75
  function renderMarkdown(content) {
28
76
  const raw = marked.parse(content != null ? content : "", { async: false });
29
77
  return DOMPurify.sanitize(raw, { USE_PROFILES: { html: true } });
30
78
  }
31
79
  var HALF_COMPONENTS = /* @__PURE__ */ new Set(["TestimonialCard"]);
32
- function HeroStatement({ headline, subheading }) {
33
- return /* @__PURE__ */ jsxs("div", { style: { paddingTop: "24px", paddingBottom: "24px" }, children: [
80
+ var flexItems = (a) => a === "center" ? "center" : a === "right" ? "flex-end" : "flex-start";
81
+ function AnnouncementBar({ text, link }) {
82
+ if (!text) return null;
83
+ return /* @__PURE__ */ jsxs("div", { style: {
84
+ display: "flex",
85
+ alignItems: "center",
86
+ justifyContent: "center",
87
+ gap: "10px",
88
+ flexWrap: "wrap",
89
+ textAlign: "center",
90
+ background: "var(--sp-accent-bg)",
91
+ border: "1px solid var(--sp-accent-border)",
92
+ borderRadius: "8px",
93
+ padding: "8px 16px",
94
+ fontSize: "14px",
95
+ lineHeight: 1.4,
96
+ color: "var(--sp-text-sub)"
97
+ }, children: [
98
+ /* @__PURE__ */ jsx("span", { children: text }),
99
+ (link == null ? void 0 : link.href) && (link == null ? void 0 : link.label) && /* @__PURE__ */ jsxs("a", { href: link.href, style: { color: "var(--sp-accent)", fontWeight: 600, textDecoration: "none", whiteSpace: "nowrap" }, children: [
100
+ link.label,
101
+ " \u2192"
102
+ ] })
103
+ ] });
104
+ }
105
+ function HeroStatement({ headline, subheading, align = "left" }) {
106
+ return /* @__PURE__ */ jsxs("div", { style: { display: "flex", flexDirection: "column", alignItems: flexItems(align), textAlign: align, paddingTop: "24px", paddingBottom: "24px" }, children: [
34
107
  /* @__PURE__ */ jsx("h2", { style: { fontSize: "32px", fontWeight: 700, letterSpacing: "-0.025em", lineHeight: 1.2, color: "var(--sp-text)", margin: "0 0 12px" }, children: headline }),
35
108
  subheading && /* @__PURE__ */ jsx("p", { style: { fontSize: "18px", color: "var(--sp-text-sub)", lineHeight: 1.6, maxWidth: "672px", margin: 0 }, children: subheading })
36
109
  ] });
37
110
  }
38
111
  var ATOM_LAYOUT_DEFAULT = ["title", "content", "stats", "cta"];
39
- function AtomCard({ title, content, layout, stats = [], ctaText, ctaUrl }) {
112
+ var ATOM_CLAMP_LINES = 4;
113
+ var clampStyle = {
114
+ display: "-webkit-box",
115
+ WebkitLineClamp: ATOM_CLAMP_LINES,
116
+ WebkitBoxOrient: "vertical",
117
+ overflow: "hidden"
118
+ };
119
+ function AtomCard({ title, content, layout, stats = [], ctaText, ctaUrl, full }) {
40
120
  const order = layout && layout.length ? layout : ATOM_LAYOUT_DEFAULT;
41
121
  const element = (name) => {
42
122
  if (name === "title") return title ? /* @__PURE__ */ jsx("h3", { style: { fontSize: "16px", fontWeight: 600, color: "var(--sp-text)", margin: "0 0 12px" }, children: title }, "title") : null;
43
- if (name === "content") return content ? /* @__PURE__ */ jsx("div", { className: "sp-prose", style: { fontSize: "15px", color: "var(--sp-text-sub)", lineHeight: 1.6 }, dangerouslySetInnerHTML: { __html: renderMarkdown(content) } }, "content") : null;
123
+ if (name === "content") return content ? /* @__PURE__ */ jsx("div", { className: "sp-prose", style: { fontSize: "15px", color: "var(--sp-text-sub)", lineHeight: 1.6, ...full ? {} : clampStyle }, dangerouslySetInnerHTML: { __html: renderMarkdown(content) } }, "content") : null;
44
124
  if (name === "stats") return stats.length ? /* @__PURE__ */ jsx("div", { style: { display: "flex", flexWrap: "wrap", gap: "20px", margin: "0 0 12px", paddingBottom: "12px", borderBottom: "1px solid var(--sp-border)" }, children: stats.map((s, i) => /* @__PURE__ */ jsxs("div", { style: { display: "flex", flexDirection: "column", gap: "2px" }, children: [
45
125
  /* @__PURE__ */ jsx("span", { style: { fontSize: "22px", fontWeight: 700, color: "var(--sp-text)", lineHeight: 1, fontVariantNumeric: "tabular-nums" }, children: s.value }),
46
126
  /* @__PURE__ */ jsx("span", { style: { fontSize: "11px", color: "var(--sp-text-muted)", textTransform: "uppercase", letterSpacing: "0.06em" }, children: s.label })
@@ -111,24 +191,24 @@ function TestimonialCard({ quote, name, role, company }) {
111
191
  ] })
112
192
  ] });
113
193
  }
114
- function ImageCtaHero({ src, alt = "", headline, subheading, ctaLabel, ctaHref }) {
194
+ function ImageCtaHero({ src, alt = "", headline, subheading, ctaLabel, ctaHref, align = "left" }) {
115
195
  if (!src) return null;
116
196
  return /* @__PURE__ */ jsxs("div", { style: { borderRadius: "12px", overflow: "hidden", position: "relative", width: "100%", paddingBottom: "56.25%" }, children: [
117
197
  /* @__PURE__ */ jsx("img", { src, alt, style: { position: "absolute", inset: 0, width: "100%", height: "100%", objectFit: "cover" } }),
118
198
  /* @__PURE__ */ jsx("div", { style: { position: "absolute", inset: 0, background: "rgba(0,0,0,0.45)" } }),
119
199
  /* @__PURE__ */ jsx("div", { style: { position: "absolute", inset: 0, background: "linear-gradient(to top, rgba(0,0,0,0.85) 0%, rgba(0,0,0,0.2) 60%, transparent 100%)" } }),
120
- /* @__PURE__ */ jsxs("div", { style: { position: "absolute", bottom: 0, left: 0, right: 0, padding: "28px" }, children: [
200
+ /* @__PURE__ */ jsxs("div", { style: { position: "absolute", bottom: 0, left: 0, right: 0, padding: "28px", textAlign: align }, children: [
121
201
  headline && /* @__PURE__ */ jsx("h2", { style: { fontSize: "24px", fontWeight: 700, color: "#fff", lineHeight: 1.2, margin: "0 0 8px", letterSpacing: "-0.02em" }, children: headline }),
122
202
  subheading && /* @__PURE__ */ jsx("p", { style: { fontSize: "15px", color: "rgba(255,255,255,0.75)", lineHeight: 1.5, margin: "0 0 20px" }, children: subheading }),
123
203
  ctaHref && ctaLabel && /* @__PURE__ */ jsx("a", { href: ctaHref, style: { display: "inline-block", background: "#fff", color: "#09090b", padding: "8px 20px", borderRadius: "8px", fontWeight: 600, fontSize: "14px", textDecoration: "none" }, children: ctaLabel })
124
204
  ] })
125
205
  ] });
126
206
  }
127
- function CtaStrip({ headline, primary, secondary }) {
128
- return /* @__PURE__ */ jsxs("div", { style: { border: "1px solid var(--sp-border)", borderRadius: "12px", padding: "32px", textAlign: "center", background: "var(--sp-bg)" }, children: [
207
+ function CtaStrip({ headline, primary, secondary, align = "center" }) {
208
+ return /* @__PURE__ */ jsxs("div", { style: { border: "1px solid var(--sp-border)", borderRadius: "12px", padding: "32px", textAlign: align, background: "var(--sp-bg)" }, children: [
129
209
  /* @__PURE__ */ jsx("h3", { style: { fontSize: "24px", fontWeight: 700, color: "var(--sp-text)", margin: "0 0 24px" }, children: headline }),
130
- /* @__PURE__ */ jsxs("div", { style: { display: "flex", alignItems: "center", justifyContent: "center", gap: "16px", flexWrap: "wrap" }, children: [
131
- /* @__PURE__ */ jsx("a", { href: primary.href, style: { background: "#fff", color: "#09090b", padding: "10px 24px", borderRadius: "12px", fontWeight: 600, fontSize: "15px", textDecoration: "none" }, children: primary.label }),
210
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", alignItems: "center", justifyContent: flexItems(align), gap: "16px", flexWrap: "wrap" }, children: [
211
+ /* @__PURE__ */ jsx("a", { href: primary.href, style: { background: "var(--sp-cta-bg)", color: "var(--sp-cta-text)", padding: "10px 24px", borderRadius: "12px", fontWeight: 600, fontSize: "15px", textDecoration: "none" }, children: primary.label }),
132
212
  secondary && /* @__PURE__ */ jsxs("a", { href: secondary.href, style: { fontSize: "15px", color: "var(--sp-text-sub)", textDecoration: "none" }, children: [
133
213
  secondary.label,
134
214
  " \u2192"
@@ -136,14 +216,17 @@ function CtaStrip({ headline, primary, secondary }) {
136
216
  ] })
137
217
  ] });
138
218
  }
139
- function renderSingle(item, brand) {
219
+ function renderSingle(item, brand, align) {
140
220
  var _a, _b;
141
221
  const s = (_a = item.slots) != null ? _a : {};
142
222
  switch (item.component) {
223
+ case "AnnouncementBar":
224
+ return /* @__PURE__ */ jsx(AnnouncementBar, { text: s.text, link: s.link });
143
225
  case "HeroStatement":
144
- return /* @__PURE__ */ jsx(HeroStatement, { headline: s.headline, subheading: s.subheading });
226
+ return /* @__PURE__ */ jsx(HeroStatement, { headline: s.headline, subheading: s.subheading, align });
227
+ // A full-width card has nothing beside it, so there is nothing for a clamp to protect.
145
228
  case "AtomCard":
146
- return /* @__PURE__ */ jsx(AtomCard, { title: s.title, content: s.content, layout: s.layout, stats: s.stats, ctaText: s.ctaText, ctaUrl: s.ctaUrl });
229
+ return /* @__PURE__ */ jsx(AtomCard, { title: s.title, content: s.content, layout: s.layout, stats: s.stats, ctaText: s.ctaText, ctaUrl: s.ctaUrl, full: s.full || resolvedSize(item) === "full" });
147
230
  case "StatGrid":
148
231
  return /* @__PURE__ */ jsx(StatGrid, { stats: s.stats, caption: s.caption });
149
232
  case "CodeBlock":
@@ -155,11 +238,23 @@ function renderSingle(item, brand) {
155
238
  case "TestimonialCard":
156
239
  return /* @__PURE__ */ jsx(TestimonialCard, { quote: s.quote, name: s.name, role: s.role, company: s.company });
157
240
  case "ImageCtaHero":
158
- return /* @__PURE__ */ jsx(ImageCtaHero, { src: s.src, alt: s.alt, headline: s.headline, subheading: s.subheading, ctaLabel: s.ctaLabel, ctaHref: s.ctaHref });
241
+ return /* @__PURE__ */ jsx(ImageCtaHero, { src: s.src, alt: s.alt, headline: s.headline, subheading: s.subheading, ctaLabel: s.ctaLabel, ctaHref: s.ctaHref, align });
159
242
  case "CtaStrip":
160
- return /* @__PURE__ */ jsx(CtaStrip, { headline: s.headline, primary: s.primary, secondary: s.secondary });
161
- default:
162
- return null;
243
+ return /* @__PURE__ */ jsx(CtaStrip, { headline: s.headline, primary: s.primary, secondary: s.secondary, align });
244
+ // An unrecognised component name used to render nothing at all. A component needs a
245
+ // renderer in three places plus the WordPress plugin, so missing one made its content
246
+ // silently absent on that surface only: no error, no warning, just a shorter page that
247
+ // looks deliberate. That is the failure mode to remove before the registry grows.
248
+ //
249
+ // Falls back to AtomCard when the slots carry something readable, since almost every
250
+ // component is built from an atom and title/content is the shape they share. When there
251
+ // is genuinely nothing to show it still renders nothing, because an empty card is worse
252
+ // than a gap, but the warning fires either way.
253
+ default: {
254
+ console.warn(`[spectare] ComponentRenderer: no renderer for "${item.component}", falling back to AtomCard`);
255
+ if (!s.title && !s.content) return null;
256
+ return /* @__PURE__ */ jsx(AtomCard, { title: s.title, content: s.content, stats: s.stats, ctaText: s.ctaText, ctaUrl: s.ctaUrl, full: true });
257
+ }
163
258
  }
164
259
  }
165
260
  function resolvedSize(item) {
@@ -191,39 +286,51 @@ function XrayBadge({ name, size }) {
191
286
  /* @__PURE__ */ jsx("span", { style: { fontSize: "10px", fontFamily: "monospace", color: "#71717a", lineHeight: 1 }, children: size })
192
287
  ] });
193
288
  }
194
- var PAIR_STYLE = ".sp-pair{display:grid;grid-template-columns:1fr 1fr;gap:20px}@media(max-width:640px){.sp-pair{grid-template-columns:1fr}}";
195
- function ComponentRenderer({ assembly, brand, xray = false, zone }) {
289
+ function ComponentRenderer({ assembly, brand, xray = false, zone, align, accent }) {
196
290
  const filtered = zone ? filterAssemblyByZone(assembly, zone) : assembly;
197
291
  const layout = buildLayout(filtered);
198
- return /* @__PURE__ */ jsxs("div", { style: { display: "flex", flexDirection: "column", gap: "20px", fontSize: "16px" }, children: [
199
- /* @__PURE__ */ jsx("style", { dangerouslySetInnerHTML: { __html: PAIR_STYLE } }),
292
+ return /* @__PURE__ */ jsxs("div", { className: SP_ROOT, style: { display: "flex", flexDirection: "column", gap: "20px", fontSize: "16px" }, children: [
293
+ /* @__PURE__ */ jsx("style", { dangerouslySetInnerHTML: { __html: buildTokenCss({ accent }) } }),
200
294
  layout.map((group, i) => {
201
295
  if (group.type === "full") {
202
296
  return /* @__PURE__ */ jsxs("div", { style: xray ? { position: "relative" } : void 0, children: [
203
297
  xray && /* @__PURE__ */ jsx(XrayBadge, { name: group.item.component, size: "full" }),
204
- renderSingle(group.item, brand)
298
+ renderSingle(group.item, brand, align)
205
299
  ] }, i);
206
300
  }
207
301
  if (group.type === "pair") {
208
302
  return /* @__PURE__ */ jsxs("div", { className: "sp-pair", children: [
209
303
  /* @__PURE__ */ jsxs("div", { style: xray ? { position: "relative" } : void 0, children: [
210
304
  xray && /* @__PURE__ */ jsx(XrayBadge, { name: group.items[0].component, size: "half" }),
211
- renderSingle(group.items[0], brand)
305
+ renderSingle(group.items[0], brand, align)
212
306
  ] }),
213
307
  /* @__PURE__ */ jsxs("div", { style: xray ? { position: "relative" } : void 0, children: [
214
308
  xray && /* @__PURE__ */ jsx(XrayBadge, { name: group.items[1].component, size: "half" }),
215
- renderSingle(group.items[1], brand)
309
+ renderSingle(group.items[1], brand, align)
216
310
  ] })
217
311
  ] }, i);
218
312
  }
219
313
  return /* @__PURE__ */ jsxs("div", { style: xray ? { position: "relative" } : void 0, children: [
220
314
  xray && /* @__PURE__ */ jsx(XrayBadge, { name: group.item.component, size: "full" }),
221
- renderSingle(group.item, brand)
315
+ renderSingle(group.item, brand, align)
222
316
  ] }, i);
223
317
  })
224
318
  ] });
225
319
  }
226
- var CSS_TOKENS = `:root{--sp-accent:#60a5fa;--sp-accent-bg:rgba(96,165,250,0.1);--sp-accent-border:rgba(96,165,250,0.2);--sp-bg:#18181b;--sp-surface:#09090b;--sp-border:#27272a;--sp-text:#f4f4f5;--sp-text-sub:#d4d4d8;--sp-text-strong:#e4e4e7;--sp-text-muted:#71717a;--sp-text-dim:#52525b;--sp-text-body:#a1a1aa;--sp-positive:#6ee7b7;--sp-divider:rgba(255,255,255,0.06);--sp-code-bg:rgba(255,255,255,0.07);--sp-code-border:rgba(255,255,255,0.08);--sp-pre-bg:rgba(0,0,0,0.4)}.sp-prose p{margin-bottom:.625rem}.sp-prose p:last-child{margin-bottom:0}.sp-prose ul{list-style:disc;padding-left:1.25rem;margin-bottom:.625rem}.sp-prose ol{list-style:decimal;padding-left:1.25rem;margin-bottom:.625rem}.sp-prose li{margin-bottom:.25rem}.sp-prose strong{color:var(--sp-text-strong);font-weight:600}.sp-prose h2,.sp-prose h3{color:var(--sp-text);font-weight:600;margin:.875rem 0 .375rem;font-size:1rem}.sp-prose code{background:var(--sp-code-bg);border:1px solid var(--sp-code-border);padding:.1rem .35rem;border-radius:.25rem;font-size:.82em;color:var(--sp-text)}.sp-prose blockquote{border-left:3px solid var(--sp-border);padding-left:1rem;color:var(--sp-text-muted);margin:.75rem 0}`;
320
+ var VARIANT_KEY = (orgSlug) => `_spectare_variant_${orgSlug}`;
321
+ function readVariantId(orgSlug) {
322
+ try {
323
+ return sessionStorage.getItem(VARIANT_KEY(orgSlug));
324
+ } catch (e) {
325
+ return null;
326
+ }
327
+ }
328
+ function writeVariantId(orgSlug, variantId) {
329
+ try {
330
+ sessionStorage.setItem(VARIANT_KEY(orgSlug), variantId);
331
+ } catch (e) {
332
+ }
333
+ }
227
334
  function buildSummary(pageHint, orgSlug) {
228
335
  const parts = [pageHint];
229
336
  const referrer = document.referrer;
@@ -275,31 +382,39 @@ function PersonalisedSection({
275
382
  style,
276
383
  initialAssembly,
277
384
  initialIntent,
278
- zone
385
+ initialVariantId,
386
+ directSummary,
387
+ zone,
388
+ align,
389
+ accent
279
390
  }) {
280
391
  const [assembly, setAssembly] = useState(initialAssembly != null ? initialAssembly : []);
281
392
  const [loading, setLoading] = useState(!initialAssembly);
282
393
  const [orgName, setOrgName] = useState(void 0);
394
+ const [debugMode, setDebugMode] = useState(false);
395
+ const [debugIntent, setDebugIntent] = useState(null);
283
396
  const tokenRef = useRef(null);
284
397
  const conversionFiredRef = useRef(false);
398
+ useEffect(() => {
399
+ setDebugMode(new URLSearchParams(window.location.search).has("spectare_debug"));
400
+ if (initialIntent) setDebugIntent(initialIntent);
401
+ }, [initialIntent]);
402
+ useEffect(() => {
403
+ if (initialVariantId) writeVariantId(orgSlug, initialVariantId);
404
+ }, [orgSlug, initialVariantId]);
285
405
  useEffect(() => {
286
406
  let cancelled = false;
287
407
  async function load() {
288
- var _a, _b, _c, _d, _e, _f, _g, _h;
408
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i;
289
409
  try {
290
- const p = new URLSearchParams(window.location.search);
291
- const utmSuffix = ["utm_source", "utm_medium", "utm_campaign", "utm_term"].map((k) => {
292
- var _a2;
293
- return (_a2 = p.get(k)) != null ? _a2 : "";
294
- }).filter(Boolean).join("_");
295
- const tokenKey = `_spectare_token_${orgSlug}${utmSuffix ? "_" + utmSuffix : ""}`;
296
- let token = sessionStorage.getItem(tokenKey);
297
- if (!token) {
298
- const summary = buildSummary(pageHint != null ? pageHint : `Visiting ${document.title}`, orgSlug);
410
+ let token = null;
411
+ if (directSummary) {
412
+ setLoading(true);
413
+ setAssembly([]);
299
414
  const res2 = await fetch(`${baseUrl}/api/qualify/${orgSlug}`, {
300
415
  method: "POST",
301
416
  headers: { "Content-Type": "application/json" },
302
- body: JSON.stringify({ summary })
417
+ body: JSON.stringify({ summary: directSummary, direct: true })
303
418
  });
304
419
  if (!res2.ok || cancelled) {
305
420
  setLoading(false);
@@ -307,8 +422,30 @@ function PersonalisedSection({
307
422
  }
308
423
  const data = await res2.json();
309
424
  token = (_a = data.token) != null ? _a : null;
310
- if (token) sessionStorage.setItem(tokenKey, token);
311
- if (data.orgName) setOrgName(data.orgName);
425
+ } else {
426
+ const p = new URLSearchParams(window.location.search);
427
+ const utmSuffix = ["utm_source", "utm_medium", "utm_campaign", "utm_term"].map((k) => {
428
+ var _a2;
429
+ return (_a2 = p.get(k)) != null ? _a2 : "";
430
+ }).filter(Boolean).join("_");
431
+ const tokenKey = `_spectare_token_${orgSlug}${utmSuffix ? "_" + utmSuffix : ""}`;
432
+ token = sessionStorage.getItem(tokenKey);
433
+ if (!token) {
434
+ const summary = buildSummary(pageHint != null ? pageHint : `Visiting ${document.title}`, orgSlug);
435
+ const res2 = await fetch(`${baseUrl}/api/qualify/${orgSlug}`, {
436
+ method: "POST",
437
+ headers: { "Content-Type": "application/json" },
438
+ body: JSON.stringify({ summary })
439
+ });
440
+ if (!res2.ok || cancelled) {
441
+ setLoading(false);
442
+ return;
443
+ }
444
+ const data = await res2.json();
445
+ token = (_b = data.token) != null ? _b : null;
446
+ if (token) sessionStorage.setItem(tokenKey, token);
447
+ if (data.orgName) setOrgName(data.orgName);
448
+ }
312
449
  }
313
450
  if (!token || cancelled) {
314
451
  setLoading(false);
@@ -318,9 +455,19 @@ function PersonalisedSection({
318
455
  let res = await fetch(`${baseUrl}/api/assemble/components`, {
319
456
  method: "POST",
320
457
  headers: { "Content-Type": "application/json" },
321
- body: JSON.stringify({ token, orgSlug })
458
+ // Direct: flag it so the server cost-caps the build, and omit the seen-variant
459
+ // exclusion so it can read the shared cache instead of forcing a fresh build.
460
+ body: JSON.stringify(
461
+ directSummary ? { token, orgSlug, direct: true } : { token, orgSlug, excludeVariantId: readVariantId(orgSlug) }
462
+ )
322
463
  });
323
- if (res.status === 401 && !cancelled) {
464
+ if (res.status === 401 && !cancelled && !directSummary) {
465
+ const p = new URLSearchParams(window.location.search);
466
+ const utmSuffix = ["utm_source", "utm_medium", "utm_campaign", "utm_term"].map((k) => {
467
+ var _a2;
468
+ return (_a2 = p.get(k)) != null ? _a2 : "";
469
+ }).filter(Boolean).join("_");
470
+ const tokenKey = `_spectare_token_${orgSlug}${utmSuffix ? "_" + utmSuffix : ""}`;
324
471
  sessionStorage.removeItem(tokenKey);
325
472
  const summary = buildSummary(pageHint != null ? pageHint : `Visiting ${document.title}`, orgSlug);
326
473
  const qRes = await fetch(`${baseUrl}/api/qualify/${orgSlug}`, {
@@ -333,7 +480,7 @@ function PersonalisedSection({
333
480
  return;
334
481
  }
335
482
  const qData = await qRes.json();
336
- token = (_b = qData.token) != null ? _b : null;
483
+ token = (_c = qData.token) != null ? _c : null;
337
484
  if (token) {
338
485
  sessionStorage.setItem(tokenKey, token);
339
486
  tokenRef.current = token;
@@ -345,14 +492,14 @@ function PersonalisedSection({
345
492
  res = await fetch(`${baseUrl}/api/assemble/components`, {
346
493
  method: "POST",
347
494
  headers: { "Content-Type": "application/json" },
348
- body: JSON.stringify({ token, orgSlug })
495
+ body: JSON.stringify({ token, orgSlug, excludeVariantId: readVariantId(orgSlug) })
349
496
  });
350
497
  }
351
498
  if (!res.ok || cancelled) {
352
499
  setLoading(false);
353
500
  return;
354
501
  }
355
- const contentType = (_c = res.headers.get("content-type")) != null ? _c : "";
502
+ const contentType = (_d = res.headers.get("content-type")) != null ? _d : "";
356
503
  if (contentType.includes("text/event-stream")) {
357
504
  const reader = res.body.getReader();
358
505
  const decoder = new TextDecoder();
@@ -364,13 +511,26 @@ function PersonalisedSection({
364
511
  if (done || cancelled) break;
365
512
  buf += decoder.decode(value, { stream: true });
366
513
  const lines = buf.split("\n");
367
- buf = (_d = lines.pop()) != null ? _d : "";
514
+ buf = (_e = lines.pop()) != null ? _e : "";
368
515
  for (const line of lines) {
369
516
  if (!line.startsWith("data: ")) continue;
370
517
  const payload = line.slice(6).trim();
371
518
  if (payload === "[DONE]") break;
372
519
  try {
373
- const comp = JSON.parse(payload);
520
+ const parsed = JSON.parse(payload);
521
+ if (parsed._variant && typeof parsed._variant === "string") {
522
+ writeVariantId(orgSlug, parsed._variant);
523
+ continue;
524
+ }
525
+ if (parsed._error) {
526
+ if (!loadingCleared) {
527
+ setLoading(false);
528
+ loadingCleared = true;
529
+ }
530
+ continue;
531
+ }
532
+ if (!parsed.component) continue;
533
+ const comp = parsed;
374
534
  if (!loadingCleared) {
375
535
  setLoading(false);
376
536
  loadingCleared = true;
@@ -386,10 +546,14 @@ function PersonalisedSection({
386
546
  }
387
547
  } else {
388
548
  const data = await res.json();
549
+ if (data.variantId) writeVariantId(orgSlug, data.variantId);
389
550
  if (cancelled) return;
390
- const clientAudience = (_f = (_e = data.intent) == null ? void 0 : _e.userContext) == null ? void 0 : _f.audience;
391
- const clientStage = (_h = (_g = data.intent) == null ? void 0 : _g.userContext) == null ? void 0 : _h.stage;
551
+ const clientAudience = (_g = (_f = data.intent) == null ? void 0 : _f.userContext) == null ? void 0 : _g.audience;
552
+ const clientStage = (_i = (_h = data.intent) == null ? void 0 : _h.userContext) == null ? void 0 : _i.stage;
392
553
  const ssrMatches = initialIntent && clientAudience === initialIntent.audience && clientStage === initialIntent.stage;
554
+ if (clientAudience && clientStage) {
555
+ setDebugIntent({ audience: clientAudience, stage: clientStage });
556
+ }
393
557
  if (!ssrMatches && Array.isArray(data.assembly) && data.assembly.length > 0) {
394
558
  setAssembly(data.assembly);
395
559
  }
@@ -403,7 +567,7 @@ function PersonalisedSection({
403
567
  return () => {
404
568
  cancelled = true;
405
569
  };
406
- }, [orgSlug, baseUrl, pageHint, initialAssembly, initialIntent]);
570
+ }, [orgSlug, baseUrl, pageHint, initialAssembly, initialIntent, directSummary]);
407
571
  useEffect(() => {
408
572
  if (!assembly.length || conversionFiredRef.current) return;
409
573
  const token = tokenRef.current;
@@ -448,6 +612,7 @@ function PersonalisedSection({
448
612
  token,
449
613
  orgSlug,
450
614
  url,
615
+ variantId: readVariantId(orgSlug),
451
616
  ...atomsShown.length ? { atomsShown } : {},
452
617
  ...visitPath.length > 1 ? { visitPath } : {},
453
618
  ...data.goalName ? { goalName: data.goalName } : {},
@@ -459,13 +624,24 @@ function PersonalisedSection({
459
624
  });
460
625
  }, [assembly, orgSlug, baseUrl]);
461
626
  if (!loading && assembly.length === 0) return null;
627
+ const debugStyle = debugMode ? { outline: "2px solid rgba(96,165,250,0.5)", borderRadius: "0.5rem", position: "relative", padding: "0.5rem", ...style } : style;
462
628
  return /* @__PURE__ */ jsxs(Fragment, { children: [
463
- /* @__PURE__ */ jsx("style", { dangerouslySetInnerHTML: { __html: CSS_TOKENS } }),
464
629
  /* @__PURE__ */ jsx("style", { dangerouslySetInnerHTML: { __html: "@keyframes sp-pulse{0%,100%{opacity:1}50%{opacity:0.4}}" } }),
465
- /* @__PURE__ */ jsx("div", { className, style, children: loading && assembly.length === 0 ? /* @__PURE__ */ jsx(Shimmer, {}) : /* @__PURE__ */ jsx(ComponentRenderer, { assembly: zone ? filterAssemblyByZone(assembly, zone) : assembly, brand: brand != null ? brand : orgName }) })
630
+ /* @__PURE__ */ jsxs("div", { className, style: debugStyle, children: [
631
+ debugMode && /* @__PURE__ */ jsxs("div", { style: { position: "absolute", top: "-1px", right: "-1px", zIndex: 20, display: "flex", alignItems: "center", gap: "0.375rem", background: "rgba(96,165,250,0.9)", borderRadius: "0 0.5rem 0 0.375rem", padding: "0.25rem 0.625rem", fontSize: "0.625rem", fontFamily: "monospace", color: "#fff", pointerEvents: "none", userSelect: "none" }, children: [
632
+ "Personalised by Spectare",
633
+ debugIntent && /* @__PURE__ */ jsxs("span", { style: { opacity: 0.75 }, children: [
634
+ " ",
635
+ debugIntent.audience,
636
+ "/",
637
+ debugIntent.stage
638
+ ] })
639
+ ] }),
640
+ loading && assembly.length === 0 ? /* @__PURE__ */ jsx(Shimmer, {}) : /* @__PURE__ */ jsx(ComponentRenderer, { assembly: zone ? filterAssemblyByZone(assembly, zone) : assembly, brand: brand != null ? brand : orgName, align, accent })
641
+ ] })
466
642
  ] });
467
643
  }
468
644
 
469
- export { ComponentRenderer, PersonalisedSection, filterAssemblyByZone };
645
+ export { AnnouncementBar, AtomCard, CodeBlock, ComparisonTable, ComponentRenderer, CtaStrip, DEFAULT_ACCENT, FeatureList, HeroStatement, ImageCtaHero, PersonalisedSection, SP_ROOT, StatGrid, TestimonialCard, buildTokenCss, filterAssemblyByZone };
470
646
  //# sourceMappingURL=index.js.map
471
647
  //# sourceMappingURL=index.js.map