@spectare-personalisation/react 0.2.2 → 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.cjs +224 -37
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +159 -5
- package/dist/index.d.ts +159 -5
- package/dist/index.js +212 -38
- package/dist/index.js.map +1 -1
- package/dist/tokens.css +78 -0
- package/dist/tokens.css.map +1 -0
- package/dist/tokens.d.cts +2 -0
- package/dist/tokens.d.ts +2 -0
- package/package.json +4 -3
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
2
|
|
|
3
|
-
type ComponentName = 'HeroStatement' | 'AtomCard' | 'StatGrid' | 'CodeBlock' | 'ComparisonTable' | 'FeatureList' | 'TestimonialCard' | 'ImageCtaHero' | 'CtaStrip';
|
|
4
|
-
type AssemblyZone = 'hero' | 'body' | 'proof' | 'cta';
|
|
3
|
+
type ComponentName = 'AnnouncementBar' | 'HeroStatement' | 'AtomCard' | 'StatGrid' | 'CodeBlock' | 'ComparisonTable' | 'FeatureList' | 'TestimonialCard' | 'ImageCtaHero' | 'CtaStrip';
|
|
4
|
+
type AssemblyZone = 'announce' | 'hero' | 'body' | 'proof' | 'cta';
|
|
5
5
|
type ComponentAssembly = Array<{
|
|
6
6
|
component: ComponentName;
|
|
7
7
|
slots?: Record<string, unknown>;
|
|
@@ -25,13 +25,119 @@ interface PersonalisedSectionProps {
|
|
|
25
25
|
audience: string;
|
|
26
26
|
stage: string;
|
|
27
27
|
};
|
|
28
|
+
/**
|
|
29
|
+
* Seed conversion attribution with a server-rendered arrangement: pass the variantId that
|
|
30
|
+
* /api/assemble/ssr returned alongside initialAssembly. Without it, a visit where the client
|
|
31
|
+
* classification matches the server's (so no client render happens) has no variant for a
|
|
32
|
+
* later conversion to credit.
|
|
33
|
+
*/
|
|
34
|
+
initialVariantId?: string;
|
|
35
|
+
/**
|
|
36
|
+
* Explicit visitor-typed context (e.g. a demo input box). Bypasses the session token cache
|
|
37
|
+
* and re-qualifies immediately with the direct flag, which the server cost-caps (cheaper
|
|
38
|
+
* model on a cache miss) and never writes to the shared cache.
|
|
39
|
+
*/
|
|
40
|
+
directSummary?: string;
|
|
28
41
|
/** Render only components assigned to this zone. */
|
|
29
42
|
zone?: AssemblyZone;
|
|
30
43
|
/** Alignment for statement-style components, to match the host page. */
|
|
31
44
|
align?: 'left' | 'center' | 'right';
|
|
45
|
+
/**
|
|
46
|
+
* Brand accent. Passed through to ComponentRenderer, which emits it into the scoped
|
|
47
|
+
* token set. Defaults to #60a5fa.
|
|
48
|
+
*/
|
|
49
|
+
accent?: string;
|
|
32
50
|
}
|
|
33
|
-
declare function PersonalisedSection({ orgSlug, baseUrl, brand, pageHint, className, style, initialAssembly, initialIntent, zone, align, }: PersonalisedSectionProps): react_jsx_runtime.JSX.Element | null;
|
|
51
|
+
declare function PersonalisedSection({ orgSlug, baseUrl, brand, pageHint, className, style, initialAssembly, initialIntent, initialVariantId, directSummary, zone, align, accent, }: PersonalisedSectionProps): react_jsx_runtime.JSX.Element | null;
|
|
34
52
|
|
|
53
|
+
type Align = 'left' | 'center' | 'right';
|
|
54
|
+
/**
|
|
55
|
+
* A single line above everything else. Deliberately the plainest component in the set: no
|
|
56
|
+
* heading, no image, no padding to speak of. A bar that draws attention to itself competes with
|
|
57
|
+
* the page it is announcing something about.
|
|
58
|
+
*
|
|
59
|
+
* Uses the accent rather than a red or amber alert colour. This is a fact worth knowing, not a
|
|
60
|
+
* warning, and a venue putting "members book from Thursday" in an alarm colour reads as a problem
|
|
61
|
+
* rather than an offer.
|
|
62
|
+
*/
|
|
63
|
+
declare function AnnouncementBar({ text, link }: {
|
|
64
|
+
text?: string;
|
|
65
|
+
link?: {
|
|
66
|
+
label: string;
|
|
67
|
+
href: string;
|
|
68
|
+
};
|
|
69
|
+
}): react_jsx_runtime.JSX.Element | null;
|
|
70
|
+
declare function HeroStatement({ headline, subheading, align }: {
|
|
71
|
+
headline: string;
|
|
72
|
+
subheading?: string;
|
|
73
|
+
align?: Align;
|
|
74
|
+
}): react_jsx_runtime.JSX.Element;
|
|
75
|
+
declare function AtomCard({ title, content, layout, stats, ctaText, ctaUrl, full }: {
|
|
76
|
+
title?: string;
|
|
77
|
+
content?: string;
|
|
78
|
+
layout?: string[];
|
|
79
|
+
stats?: {
|
|
80
|
+
value: string;
|
|
81
|
+
label: string;
|
|
82
|
+
}[];
|
|
83
|
+
ctaText?: string;
|
|
84
|
+
ctaUrl?: string;
|
|
85
|
+
full?: boolean;
|
|
86
|
+
}): react_jsx_runtime.JSX.Element;
|
|
87
|
+
declare function StatGrid({ stats, caption }: {
|
|
88
|
+
stats?: {
|
|
89
|
+
value: string;
|
|
90
|
+
label: string;
|
|
91
|
+
}[];
|
|
92
|
+
caption?: string;
|
|
93
|
+
}): react_jsx_runtime.JSX.Element;
|
|
94
|
+
declare function CodeBlock({ code, lang, caption }: {
|
|
95
|
+
code: string;
|
|
96
|
+
lang?: string;
|
|
97
|
+
caption?: string;
|
|
98
|
+
}): react_jsx_runtime.JSX.Element;
|
|
99
|
+
declare function ComparisonTable({ brand, competitor, rows }: {
|
|
100
|
+
brand?: string;
|
|
101
|
+
competitor: string;
|
|
102
|
+
rows?: {
|
|
103
|
+
label: string;
|
|
104
|
+
spectare: string;
|
|
105
|
+
them: string;
|
|
106
|
+
}[];
|
|
107
|
+
}): react_jsx_runtime.JSX.Element;
|
|
108
|
+
declare function FeatureList({ items }: {
|
|
109
|
+
items?: {
|
|
110
|
+
text: string;
|
|
111
|
+
sub?: string;
|
|
112
|
+
}[];
|
|
113
|
+
}): react_jsx_runtime.JSX.Element;
|
|
114
|
+
declare function TestimonialCard({ quote, name, role, company }: {
|
|
115
|
+
quote: string;
|
|
116
|
+
name: string;
|
|
117
|
+
role: string;
|
|
118
|
+
company?: string;
|
|
119
|
+
}): react_jsx_runtime.JSX.Element;
|
|
120
|
+
declare function ImageCtaHero({ src, alt, headline, subheading, ctaLabel, ctaHref, align }: {
|
|
121
|
+
src?: string;
|
|
122
|
+
alt?: string;
|
|
123
|
+
headline?: string;
|
|
124
|
+
subheading?: string;
|
|
125
|
+
ctaLabel?: string;
|
|
126
|
+
ctaHref?: string;
|
|
127
|
+
align?: Align;
|
|
128
|
+
}): react_jsx_runtime.JSX.Element | null;
|
|
129
|
+
declare function CtaStrip({ headline, primary, secondary, align }: {
|
|
130
|
+
headline: string;
|
|
131
|
+
primary: {
|
|
132
|
+
label: string;
|
|
133
|
+
href: string;
|
|
134
|
+
};
|
|
135
|
+
secondary?: {
|
|
136
|
+
label: string;
|
|
137
|
+
href: string;
|
|
138
|
+
};
|
|
139
|
+
align?: Align;
|
|
140
|
+
}): react_jsx_runtime.JSX.Element;
|
|
35
141
|
interface ComponentRendererProps {
|
|
36
142
|
assembly: ComponentAssembly;
|
|
37
143
|
/** Your brand name, used as the left column header in ComparisonTable. Defaults to "Us". */
|
|
@@ -46,7 +152,55 @@ interface ComponentRendererProps {
|
|
|
46
152
|
* default (HeroStatement left, CtaStrip centered).
|
|
47
153
|
*/
|
|
48
154
|
align?: 'left' | 'center' | 'right';
|
|
155
|
+
/**
|
|
156
|
+
* Brand accent, as an explicit input rather than a custom property you override and hope
|
|
157
|
+
* wins on tag order. Any CSS colour. `--sp-accent-bg` and `--sp-accent-border` are derived
|
|
158
|
+
* from it, so one value covers all three. Defaults to #60a5fa.
|
|
159
|
+
*/
|
|
160
|
+
accent?: string;
|
|
49
161
|
}
|
|
50
|
-
declare function ComponentRenderer({ assembly, brand, xray, zone, align }: ComponentRendererProps): react_jsx_runtime.JSX.Element;
|
|
162
|
+
declare function ComponentRenderer({ assembly, brand, xray, zone, align, accent }: ComponentRendererProps): react_jsx_runtime.JSX.Element;
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* The one place this package's design tokens are defined.
|
|
166
|
+
*
|
|
167
|
+
* Two things that are easy to get wrong here:
|
|
168
|
+
*
|
|
169
|
+
* 1. Tokens are declared on `.sp-root`, not `:root`. Declaring them at `:root` writes into
|
|
170
|
+
* the host's document, allows only one theme per page, and means a customer overriding
|
|
171
|
+
* `--sp-accent` competes with us at equal specificity so tag order decides the winner.
|
|
172
|
+
* Scoping also lets two differently themed sections sit on one page.
|
|
173
|
+
*
|
|
174
|
+
* 2. `tokens.css` must stay byte-identical to `buildTokenCss({ scope: STYLESHEET_SCOPE })`,
|
|
175
|
+
* which differs from the injected CSS in its selector and in nothing else.
|
|
176
|
+
* tests/unit/react-tokens.test.ts enforces both halves, because a customer who imports the
|
|
177
|
+
* stylesheet and a customer who renders the component must get the same page.
|
|
178
|
+
*
|
|
179
|
+
* components/ServerAssemblyRenderer.tsx in the Spectare app is a deliberate mirror of the
|
|
180
|
+
* values below. It is not the same file: that renderer uses rem, this one uses px.
|
|
181
|
+
*/
|
|
182
|
+
/** The package default. `spectare.js` and the WordPress plugin default to #2563eb, which is
|
|
183
|
+
* a live inconsistency and a brand decision rather than a technical one, so it is left alone
|
|
184
|
+
* here. Pass `accent` to override on either path. */
|
|
185
|
+
declare const DEFAULT_ACCENT = "#60a5fa";
|
|
186
|
+
/** Class the tokens are scoped to. Applied by ComponentRenderer to its own wrapper. */
|
|
187
|
+
declare const SP_ROOT = "sp-root";
|
|
188
|
+
/**
|
|
189
|
+
* Returns the stylesheet a renderer injects.
|
|
190
|
+
*
|
|
191
|
+
* @param accent brand colour. Accepts anything CSS does; `--sp-accent-bg` and
|
|
192
|
+
* `--sp-accent-border` are derived from it so a customer sets one value
|
|
193
|
+
* rather than three. Defaults to DEFAULT_ACCENT.
|
|
194
|
+
* @param unit length unit for layout rules. 'px' for this package, 'rem' for the app's
|
|
195
|
+
* server renderer, which sizes in rem throughout.
|
|
196
|
+
* @param scope selector the custom properties are declared on. Defaults to `.sp-root`, which is
|
|
197
|
+
* what a renderer injecting this at runtime wants. `tokens.css` is generated with
|
|
198
|
+
* STYLESHEET_SCOPE instead, so importing it works without a wrapper.
|
|
199
|
+
*/
|
|
200
|
+
declare function buildTokenCss({ accent, unit, scope }?: {
|
|
201
|
+
accent?: string;
|
|
202
|
+
unit?: 'px' | 'rem';
|
|
203
|
+
scope?: string;
|
|
204
|
+
}): string;
|
|
51
205
|
|
|
52
|
-
export { type AssemblyZone, type ComponentAssembly, type ComponentName, ComponentRenderer, type ComponentRendererProps, PersonalisedSection, type PersonalisedSectionProps, filterAssemblyByZone };
|
|
206
|
+
export { AnnouncementBar, type AssemblyZone, AtomCard, CodeBlock, ComparisonTable, type ComponentAssembly, type ComponentName, ComponentRenderer, type ComponentRendererProps, CtaStrip, DEFAULT_ACCENT, FeatureList, HeroStatement, ImageCtaHero, PersonalisedSection, type PersonalisedSectionProps, SP_ROOT, StatGrid, TestimonialCard, buildTokenCss, filterAssemblyByZone };
|
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,12 +25,83 @@ 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
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
|
+
}
|
|
33
105
|
function HeroStatement({ headline, subheading, align = "left" }) {
|
|
34
106
|
return /* @__PURE__ */ jsxs("div", { style: { display: "flex", flexDirection: "column", alignItems: flexItems(align), textAlign: align, paddingTop: "24px", paddingBottom: "24px" }, children: [
|
|
35
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 }),
|
|
@@ -37,11 +109,18 @@ function HeroStatement({ headline, subheading, align = "left" }) {
|
|
|
37
109
|
] });
|
|
38
110
|
}
|
|
39
111
|
var ATOM_LAYOUT_DEFAULT = ["title", "content", "stats", "cta"];
|
|
40
|
-
|
|
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 }) {
|
|
41
120
|
const order = layout && layout.length ? layout : ATOM_LAYOUT_DEFAULT;
|
|
42
121
|
const element = (name) => {
|
|
43
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;
|
|
44
|
-
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;
|
|
45
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: [
|
|
46
125
|
/* @__PURE__ */ jsx("span", { style: { fontSize: "22px", fontWeight: 700, color: "var(--sp-text)", lineHeight: 1, fontVariantNumeric: "tabular-nums" }, children: s.value }),
|
|
47
126
|
/* @__PURE__ */ jsx("span", { style: { fontSize: "11px", color: "var(--sp-text-muted)", textTransform: "uppercase", letterSpacing: "0.06em" }, children: s.label })
|
|
@@ -129,7 +208,7 @@ function CtaStrip({ headline, primary, secondary, align = "center" }) {
|
|
|
129
208
|
return /* @__PURE__ */ jsxs("div", { style: { border: "1px solid var(--sp-border)", borderRadius: "12px", padding: "32px", textAlign: align, background: "var(--sp-bg)" }, children: [
|
|
130
209
|
/* @__PURE__ */ jsx("h3", { style: { fontSize: "24px", fontWeight: 700, color: "var(--sp-text)", margin: "0 0 24px" }, children: headline }),
|
|
131
210
|
/* @__PURE__ */ jsxs("div", { style: { display: "flex", alignItems: "center", justifyContent: flexItems(align), gap: "16px", flexWrap: "wrap" }, children: [
|
|
132
|
-
/* @__PURE__ */ jsx("a", { href: primary.href, style: { background: "
|
|
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 }),
|
|
133
212
|
secondary && /* @__PURE__ */ jsxs("a", { href: secondary.href, style: { fontSize: "15px", color: "var(--sp-text-sub)", textDecoration: "none" }, children: [
|
|
134
213
|
secondary.label,
|
|
135
214
|
" \u2192"
|
|
@@ -141,10 +220,13 @@ function renderSingle(item, brand, align) {
|
|
|
141
220
|
var _a, _b;
|
|
142
221
|
const s = (_a = item.slots) != null ? _a : {};
|
|
143
222
|
switch (item.component) {
|
|
223
|
+
case "AnnouncementBar":
|
|
224
|
+
return /* @__PURE__ */ jsx(AnnouncementBar, { text: s.text, link: s.link });
|
|
144
225
|
case "HeroStatement":
|
|
145
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.
|
|
146
228
|
case "AtomCard":
|
|
147
|
-
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" });
|
|
148
230
|
case "StatGrid":
|
|
149
231
|
return /* @__PURE__ */ jsx(StatGrid, { stats: s.stats, caption: s.caption });
|
|
150
232
|
case "CodeBlock":
|
|
@@ -159,8 +241,20 @@ function renderSingle(item, brand, align) {
|
|
|
159
241
|
return /* @__PURE__ */ jsx(ImageCtaHero, { src: s.src, alt: s.alt, headline: s.headline, subheading: s.subheading, ctaLabel: s.ctaLabel, ctaHref: s.ctaHref, align });
|
|
160
242
|
case "CtaStrip":
|
|
161
243
|
return /* @__PURE__ */ jsx(CtaStrip, { headline: s.headline, primary: s.primary, secondary: s.secondary, align });
|
|
162
|
-
|
|
163
|
-
|
|
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
|
+
}
|
|
164
258
|
}
|
|
165
259
|
}
|
|
166
260
|
function resolvedSize(item) {
|
|
@@ -192,12 +286,11 @@ function XrayBadge({ name, size }) {
|
|
|
192
286
|
/* @__PURE__ */ jsx("span", { style: { fontSize: "10px", fontFamily: "monospace", color: "#71717a", lineHeight: 1 }, children: size })
|
|
193
287
|
] });
|
|
194
288
|
}
|
|
195
|
-
|
|
196
|
-
function ComponentRenderer({ assembly, brand, xray = false, zone, align }) {
|
|
289
|
+
function ComponentRenderer({ assembly, brand, xray = false, zone, align, accent }) {
|
|
197
290
|
const filtered = zone ? filterAssemblyByZone(assembly, zone) : assembly;
|
|
198
291
|
const layout = buildLayout(filtered);
|
|
199
|
-
return /* @__PURE__ */ jsxs("div", { style: { display: "flex", flexDirection: "column", gap: "20px", fontSize: "16px" }, children: [
|
|
200
|
-
/* @__PURE__ */ jsx("style", { dangerouslySetInnerHTML: { __html:
|
|
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 }) } }),
|
|
201
294
|
layout.map((group, i) => {
|
|
202
295
|
if (group.type === "full") {
|
|
203
296
|
return /* @__PURE__ */ jsxs("div", { style: xray ? { position: "relative" } : void 0, children: [
|
|
@@ -224,7 +317,20 @@ function ComponentRenderer({ assembly, brand, xray = false, zone, align }) {
|
|
|
224
317
|
})
|
|
225
318
|
] });
|
|
226
319
|
}
|
|
227
|
-
var
|
|
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
|
+
}
|
|
228
334
|
function buildSummary(pageHint, orgSlug) {
|
|
229
335
|
const parts = [pageHint];
|
|
230
336
|
const referrer = document.referrer;
|
|
@@ -276,32 +382,39 @@ function PersonalisedSection({
|
|
|
276
382
|
style,
|
|
277
383
|
initialAssembly,
|
|
278
384
|
initialIntent,
|
|
385
|
+
initialVariantId,
|
|
386
|
+
directSummary,
|
|
279
387
|
zone,
|
|
280
|
-
align
|
|
388
|
+
align,
|
|
389
|
+
accent
|
|
281
390
|
}) {
|
|
282
391
|
const [assembly, setAssembly] = useState(initialAssembly != null ? initialAssembly : []);
|
|
283
392
|
const [loading, setLoading] = useState(!initialAssembly);
|
|
284
393
|
const [orgName, setOrgName] = useState(void 0);
|
|
394
|
+
const [debugMode, setDebugMode] = useState(false);
|
|
395
|
+
const [debugIntent, setDebugIntent] = useState(null);
|
|
285
396
|
const tokenRef = useRef(null);
|
|
286
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]);
|
|
287
405
|
useEffect(() => {
|
|
288
406
|
let cancelled = false;
|
|
289
407
|
async function load() {
|
|
290
|
-
var _a, _b, _c, _d, _e, _f, _g, _h;
|
|
408
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _i;
|
|
291
409
|
try {
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
}).filter(Boolean).join("_");
|
|
297
|
-
const tokenKey = `_spectare_token_${orgSlug}${utmSuffix ? "_" + utmSuffix : ""}`;
|
|
298
|
-
let token = sessionStorage.getItem(tokenKey);
|
|
299
|
-
if (!token) {
|
|
300
|
-
const summary = buildSummary(pageHint != null ? pageHint : `Visiting ${document.title}`, orgSlug);
|
|
410
|
+
let token = null;
|
|
411
|
+
if (directSummary) {
|
|
412
|
+
setLoading(true);
|
|
413
|
+
setAssembly([]);
|
|
301
414
|
const res2 = await fetch(`${baseUrl}/api/qualify/${orgSlug}`, {
|
|
302
415
|
method: "POST",
|
|
303
416
|
headers: { "Content-Type": "application/json" },
|
|
304
|
-
body: JSON.stringify({ summary })
|
|
417
|
+
body: JSON.stringify({ summary: directSummary, direct: true })
|
|
305
418
|
});
|
|
306
419
|
if (!res2.ok || cancelled) {
|
|
307
420
|
setLoading(false);
|
|
@@ -309,8 +422,30 @@ function PersonalisedSection({
|
|
|
309
422
|
}
|
|
310
423
|
const data = await res2.json();
|
|
311
424
|
token = (_a = data.token) != null ? _a : null;
|
|
312
|
-
|
|
313
|
-
|
|
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
|
+
}
|
|
314
449
|
}
|
|
315
450
|
if (!token || cancelled) {
|
|
316
451
|
setLoading(false);
|
|
@@ -320,9 +455,19 @@ function PersonalisedSection({
|
|
|
320
455
|
let res = await fetch(`${baseUrl}/api/assemble/components`, {
|
|
321
456
|
method: "POST",
|
|
322
457
|
headers: { "Content-Type": "application/json" },
|
|
323
|
-
|
|
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
|
+
)
|
|
324
463
|
});
|
|
325
|
-
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 : ""}`;
|
|
326
471
|
sessionStorage.removeItem(tokenKey);
|
|
327
472
|
const summary = buildSummary(pageHint != null ? pageHint : `Visiting ${document.title}`, orgSlug);
|
|
328
473
|
const qRes = await fetch(`${baseUrl}/api/qualify/${orgSlug}`, {
|
|
@@ -335,7 +480,7 @@ function PersonalisedSection({
|
|
|
335
480
|
return;
|
|
336
481
|
}
|
|
337
482
|
const qData = await qRes.json();
|
|
338
|
-
token = (
|
|
483
|
+
token = (_c = qData.token) != null ? _c : null;
|
|
339
484
|
if (token) {
|
|
340
485
|
sessionStorage.setItem(tokenKey, token);
|
|
341
486
|
tokenRef.current = token;
|
|
@@ -347,14 +492,14 @@ function PersonalisedSection({
|
|
|
347
492
|
res = await fetch(`${baseUrl}/api/assemble/components`, {
|
|
348
493
|
method: "POST",
|
|
349
494
|
headers: { "Content-Type": "application/json" },
|
|
350
|
-
body: JSON.stringify({ token, orgSlug })
|
|
495
|
+
body: JSON.stringify({ token, orgSlug, excludeVariantId: readVariantId(orgSlug) })
|
|
351
496
|
});
|
|
352
497
|
}
|
|
353
498
|
if (!res.ok || cancelled) {
|
|
354
499
|
setLoading(false);
|
|
355
500
|
return;
|
|
356
501
|
}
|
|
357
|
-
const contentType = (
|
|
502
|
+
const contentType = (_d = res.headers.get("content-type")) != null ? _d : "";
|
|
358
503
|
if (contentType.includes("text/event-stream")) {
|
|
359
504
|
const reader = res.body.getReader();
|
|
360
505
|
const decoder = new TextDecoder();
|
|
@@ -366,13 +511,26 @@ function PersonalisedSection({
|
|
|
366
511
|
if (done || cancelled) break;
|
|
367
512
|
buf += decoder.decode(value, { stream: true });
|
|
368
513
|
const lines = buf.split("\n");
|
|
369
|
-
buf = (
|
|
514
|
+
buf = (_e = lines.pop()) != null ? _e : "";
|
|
370
515
|
for (const line of lines) {
|
|
371
516
|
if (!line.startsWith("data: ")) continue;
|
|
372
517
|
const payload = line.slice(6).trim();
|
|
373
518
|
if (payload === "[DONE]") break;
|
|
374
519
|
try {
|
|
375
|
-
const
|
|
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;
|
|
376
534
|
if (!loadingCleared) {
|
|
377
535
|
setLoading(false);
|
|
378
536
|
loadingCleared = true;
|
|
@@ -388,10 +546,14 @@ function PersonalisedSection({
|
|
|
388
546
|
}
|
|
389
547
|
} else {
|
|
390
548
|
const data = await res.json();
|
|
549
|
+
if (data.variantId) writeVariantId(orgSlug, data.variantId);
|
|
391
550
|
if (cancelled) return;
|
|
392
|
-
const clientAudience = (
|
|
393
|
-
const clientStage = (
|
|
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;
|
|
394
553
|
const ssrMatches = initialIntent && clientAudience === initialIntent.audience && clientStage === initialIntent.stage;
|
|
554
|
+
if (clientAudience && clientStage) {
|
|
555
|
+
setDebugIntent({ audience: clientAudience, stage: clientStage });
|
|
556
|
+
}
|
|
395
557
|
if (!ssrMatches && Array.isArray(data.assembly) && data.assembly.length > 0) {
|
|
396
558
|
setAssembly(data.assembly);
|
|
397
559
|
}
|
|
@@ -405,7 +567,7 @@ function PersonalisedSection({
|
|
|
405
567
|
return () => {
|
|
406
568
|
cancelled = true;
|
|
407
569
|
};
|
|
408
|
-
}, [orgSlug, baseUrl, pageHint, initialAssembly, initialIntent]);
|
|
570
|
+
}, [orgSlug, baseUrl, pageHint, initialAssembly, initialIntent, directSummary]);
|
|
409
571
|
useEffect(() => {
|
|
410
572
|
if (!assembly.length || conversionFiredRef.current) return;
|
|
411
573
|
const token = tokenRef.current;
|
|
@@ -450,6 +612,7 @@ function PersonalisedSection({
|
|
|
450
612
|
token,
|
|
451
613
|
orgSlug,
|
|
452
614
|
url,
|
|
615
|
+
variantId: readVariantId(orgSlug),
|
|
453
616
|
...atomsShown.length ? { atomsShown } : {},
|
|
454
617
|
...visitPath.length > 1 ? { visitPath } : {},
|
|
455
618
|
...data.goalName ? { goalName: data.goalName } : {},
|
|
@@ -461,13 +624,24 @@ function PersonalisedSection({
|
|
|
461
624
|
});
|
|
462
625
|
}, [assembly, orgSlug, baseUrl]);
|
|
463
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;
|
|
464
628
|
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
465
|
-
/* @__PURE__ */ jsx("style", { dangerouslySetInnerHTML: { __html: CSS_TOKENS } }),
|
|
466
629
|
/* @__PURE__ */ jsx("style", { dangerouslySetInnerHTML: { __html: "@keyframes sp-pulse{0%,100%{opacity:1}50%{opacity:0.4}}" } }),
|
|
467
|
-
/* @__PURE__ */
|
|
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
|
+
] })
|
|
468
642
|
] });
|
|
469
643
|
}
|
|
470
644
|
|
|
471
|
-
export { ComponentRenderer, PersonalisedSection, filterAssemblyByZone };
|
|
645
|
+
export { AnnouncementBar, AtomCard, CodeBlock, ComparisonTable, ComponentRenderer, CtaStrip, DEFAULT_ACCENT, FeatureList, HeroStatement, ImageCtaHero, PersonalisedSection, SP_ROOT, StatGrid, TestimonialCard, buildTokenCss, filterAssemblyByZone };
|
|
472
646
|
//# sourceMappingURL=index.js.map
|
|
473
647
|
//# sourceMappingURL=index.js.map
|