@burdenoff/website-sdk 2026.925.3 → 2026.927.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/contact.js +8 -0
- package/dist/components/contact.js.map +1 -1
- package/dist/components/contact.mjs +8 -0
- package/dist/components/contact.mjs.map +1 -1
- package/dist/components/explore.js +55 -18
- package/dist/components/explore.js.map +1 -1
- package/dist/components/explore.mjs +55 -18
- package/dist/components/explore.mjs.map +1 -1
- package/dist/components/newsletter.js +8 -0
- package/dist/components/newsletter.js.map +1 -1
- package/dist/components/newsletter.mjs +8 -0
- package/dist/components/newsletter.mjs.map +1 -1
- package/dist/components/partners.js +8 -0
- package/dist/components/partners.js.map +1 -1
- package/dist/components/partners.mjs +8 -0
- package/dist/components/partners.mjs.map +1 -1
- package/dist/components/resource-links.js +8 -0
- package/dist/components/resource-links.js.map +1 -1
- package/dist/components/resource-links.mjs +8 -0
- package/dist/components/resource-links.mjs.map +1 -1
- package/dist/components/social-links.js +8 -0
- package/dist/components/social-links.js.map +1 -1
- package/dist/components/social-links.mjs +8 -0
- package/dist/components/social-links.mjs.map +1 -1
- package/dist/components/waitlist.js +8 -0
- package/dist/components/waitlist.js.map +1 -1
- package/dist/components/waitlist.mjs +8 -0
- package/dist/components/waitlist.mjs.map +1 -1
- package/dist/content/index.js +8 -0
- package/dist/content/index.js.map +1 -1
- package/dist/content/index.mjs +8 -0
- package/dist/content/index.mjs.map +1 -1
- package/dist/hooks/index.js +30 -1
- package/dist/hooks/index.js.map +1 -1
- package/dist/hooks/index.mjs +30 -1
- package/dist/hooks/index.mjs.map +1 -1
- package/dist/index.d.mts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +103 -35
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +95 -36
- package/dist/index.mjs.map +1 -1
- package/dist/product/index.d.mts +16 -1
- package/dist/product/index.d.ts +16 -1
- package/dist/product/index.js +61 -17
- package/dist/product/index.js.map +1 -1
- package/dist/product/index.mjs +61 -18
- package/dist/product/index.mjs.map +1 -1
- package/dist/store/index.js +8 -0
- package/dist/store/index.js.map +1 -1
- package/dist/store/index.mjs +8 -0
- package/dist/store/index.mjs.map +1 -1
- package/dist/utils/index.d.mts +43 -1
- package/dist/utils/index.d.ts +43 -1
- package/dist/utils/index.js +42 -0
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/index.mjs +35 -1
- package/dist/utils/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/utils/index.d.mts
CHANGED
|
@@ -120,4 +120,46 @@ declare function isFooterlessPath(pathname: string): boolean;
|
|
|
120
120
|
/** Inverse of {@link isFooterlessPath}, for layouts that read better positively. */
|
|
121
121
|
declare function shouldRenderSiteFooter(pathname: string): boolean;
|
|
122
122
|
|
|
123
|
-
|
|
123
|
+
/**
|
|
124
|
+
* Product slug aliases for products whose machine slug is being renamed
|
|
125
|
+
* (today: `botlit` → `botmanor`, see botmanor-migration PLAN.md Stage 0 / WP-E).
|
|
126
|
+
*
|
|
127
|
+
* - Readers accept both slugs: compare with {@link isSameProductSlug}, look up
|
|
128
|
+
* with {@link productSlugVariants}.
|
|
129
|
+
* - Writers emit {@link emitProductSlug}, which follows {@link PRODUCT_SLUG_EMIT}
|
|
130
|
+
* (`"current"` since PLAN.md Stage 5a, so writers emit `botmanor`).
|
|
131
|
+
*
|
|
132
|
+
* Mirrors `@burdenoff/be-sdk/product-alias` (product-slug concern only) so the
|
|
133
|
+
* browser bundle does not pull in the backend SDK.
|
|
134
|
+
*/
|
|
135
|
+
/** One product whose slug is being renamed. Both values are lowercase. */
|
|
136
|
+
interface ProductSlugRename {
|
|
137
|
+
readonly legacy: string;
|
|
138
|
+
readonly current: string;
|
|
139
|
+
}
|
|
140
|
+
/** Every product slug rename in flight. */
|
|
141
|
+
declare const PRODUCT_SLUG_RENAMES: readonly ProductSlugRename[];
|
|
142
|
+
/** Legacy slug → current slug, e.g. `{ botlit: "botmanor" }`. */
|
|
143
|
+
declare const PRODUCT_SLUG_ALIASES: Readonly<Record<string, string>>;
|
|
144
|
+
/** Which spelling a writer emits. */
|
|
145
|
+
type ProductSlugGeneration = "legacy" | "current";
|
|
146
|
+
/**
|
|
147
|
+
* Which slug writers emit. Flipped to `"current"` at PLAN.md Stage 5a: stored
|
|
148
|
+
* product slugs are migrated to `botmanor`, and every reader accepts both.
|
|
149
|
+
*/
|
|
150
|
+
declare const PRODUCT_SLUG_EMIT: Readonly<{
|
|
151
|
+
productSlug: ProductSlugGeneration;
|
|
152
|
+
}>;
|
|
153
|
+
/** `botlit` (any case) → `botmanor`; every other slug is returned unchanged. */
|
|
154
|
+
declare function normalizeProductSlug(slug: string): string;
|
|
155
|
+
declare function normalizeProductSlug(slug: string | null | undefined): string | null | undefined;
|
|
156
|
+
/** Old spelling of a renamed slug (`botmanor` → `botlit`); every other slug unchanged. */
|
|
157
|
+
declare function legacyProductSlug(slug: string): string;
|
|
158
|
+
/** Every slug a reader must accept for `slug`, the given one first. */
|
|
159
|
+
declare function productSlugVariants(slug: string): string[];
|
|
160
|
+
/** True when `a` and `b` are the same product under the rename. */
|
|
161
|
+
declare function isSameProductSlug(a: string | null | undefined, b: string | null | undefined): boolean;
|
|
162
|
+
/** The slug writers should use today, from either spelling. */
|
|
163
|
+
declare function emitProductSlug(slug: string): string;
|
|
164
|
+
|
|
165
|
+
export { EXPLORE_RECAPTCHA_ACTION, FOOTERLESS_ROUTES, PRODUCT_SLUG_ALIASES, PRODUCT_SLUG_EMIT, PRODUCT_SLUG_RENAMES, type ProductSlugGeneration, type ProductSlugRename, type RecaptchaV3, cn, emitProductSlug, getRecaptchaV3Token, isFooterlessPath, isSameProductSlug, legacyProductSlug, loadRecaptchaV3, normalizeProductSlug, normalizeRoutePath, productSlugVariants, shouldRenderSiteFooter };
|
package/dist/utils/index.d.ts
CHANGED
|
@@ -120,4 +120,46 @@ declare function isFooterlessPath(pathname: string): boolean;
|
|
|
120
120
|
/** Inverse of {@link isFooterlessPath}, for layouts that read better positively. */
|
|
121
121
|
declare function shouldRenderSiteFooter(pathname: string): boolean;
|
|
122
122
|
|
|
123
|
-
|
|
123
|
+
/**
|
|
124
|
+
* Product slug aliases for products whose machine slug is being renamed
|
|
125
|
+
* (today: `botlit` → `botmanor`, see botmanor-migration PLAN.md Stage 0 / WP-E).
|
|
126
|
+
*
|
|
127
|
+
* - Readers accept both slugs: compare with {@link isSameProductSlug}, look up
|
|
128
|
+
* with {@link productSlugVariants}.
|
|
129
|
+
* - Writers emit {@link emitProductSlug}, which follows {@link PRODUCT_SLUG_EMIT}
|
|
130
|
+
* (`"current"` since PLAN.md Stage 5a, so writers emit `botmanor`).
|
|
131
|
+
*
|
|
132
|
+
* Mirrors `@burdenoff/be-sdk/product-alias` (product-slug concern only) so the
|
|
133
|
+
* browser bundle does not pull in the backend SDK.
|
|
134
|
+
*/
|
|
135
|
+
/** One product whose slug is being renamed. Both values are lowercase. */
|
|
136
|
+
interface ProductSlugRename {
|
|
137
|
+
readonly legacy: string;
|
|
138
|
+
readonly current: string;
|
|
139
|
+
}
|
|
140
|
+
/** Every product slug rename in flight. */
|
|
141
|
+
declare const PRODUCT_SLUG_RENAMES: readonly ProductSlugRename[];
|
|
142
|
+
/** Legacy slug → current slug, e.g. `{ botlit: "botmanor" }`. */
|
|
143
|
+
declare const PRODUCT_SLUG_ALIASES: Readonly<Record<string, string>>;
|
|
144
|
+
/** Which spelling a writer emits. */
|
|
145
|
+
type ProductSlugGeneration = "legacy" | "current";
|
|
146
|
+
/**
|
|
147
|
+
* Which slug writers emit. Flipped to `"current"` at PLAN.md Stage 5a: stored
|
|
148
|
+
* product slugs are migrated to `botmanor`, and every reader accepts both.
|
|
149
|
+
*/
|
|
150
|
+
declare const PRODUCT_SLUG_EMIT: Readonly<{
|
|
151
|
+
productSlug: ProductSlugGeneration;
|
|
152
|
+
}>;
|
|
153
|
+
/** `botlit` (any case) → `botmanor`; every other slug is returned unchanged. */
|
|
154
|
+
declare function normalizeProductSlug(slug: string): string;
|
|
155
|
+
declare function normalizeProductSlug(slug: string | null | undefined): string | null | undefined;
|
|
156
|
+
/** Old spelling of a renamed slug (`botmanor` → `botlit`); every other slug unchanged. */
|
|
157
|
+
declare function legacyProductSlug(slug: string): string;
|
|
158
|
+
/** Every slug a reader must accept for `slug`, the given one first. */
|
|
159
|
+
declare function productSlugVariants(slug: string): string[];
|
|
160
|
+
/** True when `a` and `b` are the same product under the rename. */
|
|
161
|
+
declare function isSameProductSlug(a: string | null | undefined, b: string | null | undefined): boolean;
|
|
162
|
+
/** The slug writers should use today, from either spelling. */
|
|
163
|
+
declare function emitProductSlug(slug: string): string;
|
|
164
|
+
|
|
165
|
+
export { EXPLORE_RECAPTCHA_ACTION, FOOTERLESS_ROUTES, PRODUCT_SLUG_ALIASES, PRODUCT_SLUG_EMIT, PRODUCT_SLUG_RENAMES, type ProductSlugGeneration, type ProductSlugRename, type RecaptchaV3, cn, emitProductSlug, getRecaptchaV3Token, isFooterlessPath, isSameProductSlug, legacyProductSlug, loadRecaptchaV3, normalizeProductSlug, normalizeRoutePath, productSlugVariants, shouldRenderSiteFooter };
|
package/dist/utils/index.js
CHANGED
|
@@ -87,13 +87,55 @@ function shouldRenderSiteFooter(pathname) {
|
|
|
87
87
|
return !isFooterlessPath(pathname);
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
+
// src/utils/product-slug.ts
|
|
91
|
+
var PRODUCT_SLUG_RENAMES = Object.freeze(
|
|
92
|
+
[Object.freeze({ legacy: "botlit", current: "botmanor" })]
|
|
93
|
+
);
|
|
94
|
+
var PRODUCT_SLUG_ALIASES = Object.freeze(
|
|
95
|
+
Object.fromEntries(PRODUCT_SLUG_RENAMES.map((r) => [r.legacy, r.current]))
|
|
96
|
+
);
|
|
97
|
+
var PRODUCT_SLUG_EMIT = Object.freeze({ productSlug: "current" });
|
|
98
|
+
function findRename(slug) {
|
|
99
|
+
const wanted = slug.toLowerCase();
|
|
100
|
+
return PRODUCT_SLUG_RENAMES.find(
|
|
101
|
+
(r) => r.legacy === wanted || r.current === wanted
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
function normalizeProductSlug(slug) {
|
|
105
|
+
if (slug == null) return slug;
|
|
106
|
+
return findRename(slug)?.current ?? slug;
|
|
107
|
+
}
|
|
108
|
+
function legacyProductSlug(slug) {
|
|
109
|
+
return findRename(slug)?.legacy ?? slug;
|
|
110
|
+
}
|
|
111
|
+
function productSlugVariants(slug) {
|
|
112
|
+
return [
|
|
113
|
+
.../* @__PURE__ */ new Set([slug, normalizeProductSlug(slug), legacyProductSlug(slug)])
|
|
114
|
+
];
|
|
115
|
+
}
|
|
116
|
+
function isSameProductSlug(a, b) {
|
|
117
|
+
if (a == null || b == null) return a === b;
|
|
118
|
+
return a === b || normalizeProductSlug(a) === normalizeProductSlug(b);
|
|
119
|
+
}
|
|
120
|
+
function emitProductSlug(slug) {
|
|
121
|
+
return PRODUCT_SLUG_EMIT.productSlug === "current" ? normalizeProductSlug(slug) : legacyProductSlug(slug);
|
|
122
|
+
}
|
|
123
|
+
|
|
90
124
|
exports.EXPLORE_RECAPTCHA_ACTION = EXPLORE_RECAPTCHA_ACTION;
|
|
91
125
|
exports.FOOTERLESS_ROUTES = FOOTERLESS_ROUTES;
|
|
126
|
+
exports.PRODUCT_SLUG_ALIASES = PRODUCT_SLUG_ALIASES;
|
|
127
|
+
exports.PRODUCT_SLUG_EMIT = PRODUCT_SLUG_EMIT;
|
|
128
|
+
exports.PRODUCT_SLUG_RENAMES = PRODUCT_SLUG_RENAMES;
|
|
92
129
|
exports.cn = cn;
|
|
130
|
+
exports.emitProductSlug = emitProductSlug;
|
|
93
131
|
exports.getRecaptchaV3Token = getRecaptchaV3Token;
|
|
94
132
|
exports.isFooterlessPath = isFooterlessPath;
|
|
133
|
+
exports.isSameProductSlug = isSameProductSlug;
|
|
134
|
+
exports.legacyProductSlug = legacyProductSlug;
|
|
95
135
|
exports.loadRecaptchaV3 = loadRecaptchaV3;
|
|
136
|
+
exports.normalizeProductSlug = normalizeProductSlug;
|
|
96
137
|
exports.normalizeRoutePath = normalizeRoutePath;
|
|
138
|
+
exports.productSlugVariants = productSlugVariants;
|
|
97
139
|
exports.shouldRenderSiteFooter = shouldRenderSiteFooter;
|
|
98
140
|
//# sourceMappingURL=index.js.map
|
|
99
141
|
//# sourceMappingURL=index.js.map
|
package/dist/utils/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/utils/cn.ts","../../src/utils/recaptcha-v3.ts","../../src/utils/site-chrome.ts"],"names":["twMerge","clsx"],"mappings":"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","file":"index.js","sourcesContent":["/**\n * Utility function to merge Tailwind CSS classes\n * Combines clsx and tailwind-merge for optimal class handling\n */\n\nimport { type ClassValue, clsx } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\n/**\n * Merge multiple class names intelligently\n * - Handles conditional classes via clsx\n * - Deduplicates and resolves Tailwind conflicts via tailwind-merge\n *\n * @example\n * ```ts\n * cn('px-4 py-2', 'bg-blue-500', { 'text-white': isActive })\n * // => 'px-4 py-2 bg-blue-500 text-white' (if isActive is true)\n * ```\n */\nexport function cn(...inputs: ClassValue[]): string {\n return twMerge(clsx(inputs));\n}\n","/**\n * reCAPTCHA v3 (invisible, score-based) loader and token minter.\n *\n * Deliberately dependency-free: the SDK already ships the `react-google-recaptcha`\n * peer for the **v2 checkbox** used by `ContactPage`, and the two widgets cannot\n * share a script tag — v3 needs `?render=<siteKey>` on the URL, v2 must not have\n * it. This module injects its own v3 script and leaves the v2 integration alone.\n *\n * The Explore chat turns v3 on purely from the backend catalog\n * (`exploreCatalog.captchaRequired && recaptchaSiteKey`), so nothing is loaded\n * and no third-party request is made on sites where captcha is off.\n */\n\n/** The slice of the `grecaptcha` global this module uses. */\nexport interface RecaptchaV3 {\n /** Runs the callback once the API is fully initialised. */\n ready(callback: () => void): void;\n /** Mints a single-use token for `action`. */\n execute(siteKey: string, options: { action: string }): Promise<string>;\n}\n\ndeclare global {\n interface Window {\n grecaptcha?: RecaptchaV3;\n }\n}\n\nconst SCRIPT_ID = \"boff-recaptcha-v3\";\nconst SCRIPT_SRC = \"https://www.google.com/recaptcha/api.js?render=\";\n\n/** Default action name reported to reCAPTCHA for Explore chat submissions. */\nexport const EXPLORE_RECAPTCHA_ACTION = \"explore_chat\";\n\n/**\n * One in-flight/settled promise per site key. Guarantees the script tag is\n * injected at most once even when several components mount at the same time,\n * and lets later callers await the same load instead of racing it.\n */\nconst loaders = new Map<string, Promise<RecaptchaV3>>();\n\nfunction isBrowser(): boolean {\n return typeof window !== \"undefined\" && typeof document !== \"undefined\";\n}\n\n/**\n * Load the reCAPTCHA v3 script for `siteKey` and resolve once `grecaptcha` is\n * ready to mint tokens.\n *\n * - Injects the script at most once per site key (subsequent calls reuse the\n * same promise).\n * - Adopts an already-present script tag (e.g. injected by the host site)\n * rather than adding a second one.\n * - Rejects rather than hanging when the script fails to load, so the caller\n * can decide whether to submit without a token or surface an error.\n */\nexport function loadRecaptchaV3(siteKey: string): Promise<RecaptchaV3> {\n if (!siteKey) {\n return Promise.reject(new Error(\"reCAPTCHA site key is required\"));\n }\n if (!isBrowser()) {\n return Promise.reject(\n new Error(\"reCAPTCHA v3 can only be loaded in a browser\"),\n );\n }\n\n const existing = loaders.get(siteKey);\n if (existing) return existing;\n\n const promise = new Promise<RecaptchaV3>((resolve, reject) => {\n const ready = () => {\n const api = window.grecaptcha;\n if (!api) {\n reject(new Error(\"reCAPTCHA loaded but grecaptcha is unavailable\"));\n return;\n }\n // `ready` queues until the API has finished initialising internally;\n // calling `execute` before that throws.\n api.ready(() => resolve(api));\n };\n\n // Already loaded by this module, another SDK instance, or the host site.\n if (window.grecaptcha) {\n ready();\n return;\n }\n\n const scriptId = `${SCRIPT_ID}-${siteKey}`;\n const found = document.getElementById(scriptId);\n if (found) {\n found.addEventListener(\"load\", ready, { once: true });\n found.addEventListener(\n \"error\",\n () => reject(new Error(\"Failed to load reCAPTCHA v3\")),\n { once: true },\n );\n return;\n }\n\n const script = document.createElement(\"script\");\n script.id = scriptId;\n script.src = `${SCRIPT_SRC}${encodeURIComponent(siteKey)}`;\n script.async = true;\n script.defer = true;\n script.addEventListener(\"load\", ready, { once: true });\n script.addEventListener(\n \"error\",\n () => {\n // Drop the memo so a later attempt can retry a transient CDN failure\n // instead of being stuck with a permanently rejected promise.\n loaders.delete(siteKey);\n reject(new Error(\"Failed to load reCAPTCHA v3\"));\n },\n { once: true },\n );\n document.head.appendChild(script);\n });\n\n loaders.set(siteKey, promise);\n return promise;\n}\n\n/**\n * Mint a single-use reCAPTCHA v3 token, loading the script on first use.\n *\n * Tokens are valid for ~2 minutes and are consumed by one verification, so\n * this must be called per submission — never cached.\n */\nexport async function getRecaptchaV3Token(\n siteKey: string,\n action: string = EXPLORE_RECAPTCHA_ACTION,\n): Promise<string> {\n const api = await loadRecaptchaV3(siteKey);\n return api.execute(siteKey, { action });\n}\n","/**\n * Which routes a host site should render WITHOUT its marketing footer.\n *\n * ## Why this lives in the SDK\n *\n * It was written 39 times. The footer belongs to each website, so the obvious\n * place for the rule is each website — and eight agents asked to add it produced\n * five different helpers (`footerless-routes.ts`, `footer-visibility.ts`,\n * `footer-routes.ts`, `explore-route.ts`, plus six copies inlined straight into\n * a layout). All five behave the same today. The next person to change the rule\n * has to find all five, and will not.\n *\n * The rule is really a property of the SDK's own surfaces: `/explore` is\n * footerless because `ExplorePage` sizes itself to one viewport, which is a fact\n * about this package, not about any site. So it is declared here, once, and the\n * sites ask.\n *\n * ## Why `/explore` and nothing else\n *\n * `ExplorePage` renders a full-viewport chat pane (`100dvh` minus the site\n * chrome above it), so anything after it is below the fold by construction. On a\n * phone these footers are 1100-1330px — roughly 70% of the document — and that\n * bulk is what allowed a restored scroll offset to open the page on nothing but\n * footer, with zero pixels of the chat on screen.\n *\n * The SDK's OTHER pages are the opposite case and must keep their footer:\n * `/concepts`, `/concept/:slug`, `/answers` and `/answer/:slug` are ordinary\n * indexed content pages whose footer carries the site's internal links.\n * Stripping it would cost hundreds of pages their internal linking, and nobody\n * would notice for weeks.\n */\n\n/** Routes that render without the site footer. Exact paths, not prefixes. */\nexport const FOOTERLESS_ROUTES: readonly string[] = [\"/explore\"];\n\n/**\n * Normalise a router pathname for exact comparison.\n *\n * React Router matches case-insensitively (`caseSensitive` defaults to false)\n * and tolerates a trailing slash, so `/Explore/` reaches the same route element\n * as `/explore` and has to be treated the same way here. Half the fleet's\n * hand-written versions compared case-sensitively and would have rendered a\n * footer under the chat pane on `/Explore`.\n */\nexport function normalizeRoutePath(pathname: string): string {\n const trimmed = pathname.toLowerCase().replace(/\\/+$/, \"\");\n return trimmed === \"\" ? \"/\" : trimmed;\n}\n\n/**\n * True when this pathname is one of the SDK's full-screen surfaces.\n *\n * Deliberately an exact-set test. `startsWith(\"/explore\")` would also swallow a\n * future `/explore-beta`, and `includes(\"explore\")` would swallow far more —\n * including `/concept/explore-your-data`, which is a real shape of concept slug.\n */\nexport function isFooterlessPath(pathname: string): boolean {\n return FOOTERLESS_ROUTES.includes(normalizeRoutePath(pathname));\n}\n\n/** Inverse of {@link isFooterlessPath}, for layouts that read better positively. */\nexport function shouldRenderSiteFooter(pathname: string): boolean {\n return !isFooterlessPath(pathname);\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../../src/utils/cn.ts","../../src/utils/recaptcha-v3.ts","../../src/utils/site-chrome.ts","../../src/utils/product-slug.ts"],"names":["twMerge","clsx"],"mappings":";;;;;AAmBO,SAAS,MAAM,MAAA,EAA8B;AAClD,EAAA,OAAOA,qBAAA,CAAQC,SAAA,CAAK,MAAM,CAAC,CAAA;AAC7B;;;ACMA,IAAM,SAAA,GAAY,mBAAA;AAClB,IAAM,UAAA,GAAa,iDAAA;AAGZ,IAAM,wBAAA,GAA2B;AAOxC,IAAM,OAAA,uBAAc,GAAA,EAAkC;AAEtD,SAAS,SAAA,GAAqB;AAC5B,EAAA,OAAO,OAAO,MAAA,KAAW,WAAA,IAAe,OAAO,QAAA,KAAa,WAAA;AAC9D;AAaO,SAAS,gBAAgB,OAAA,EAAuC;AACrE,EAAA,IAAI,CAAC,OAAA,EAAS;AACZ,IAAA,OAAO,OAAA,CAAQ,MAAA,CAAO,IAAI,KAAA,CAAM,gCAAgC,CAAC,CAAA;AAAA,EACnE;AACA,EAAA,IAAI,CAAC,WAAU,EAAG;AAChB,IAAA,OAAO,OAAA,CAAQ,MAAA;AAAA,MACb,IAAI,MAAM,8CAA8C;AAAA,KAC1D;AAAA,EACF;AAEA,EAAA,MAAM,QAAA,GAAW,OAAA,CAAQ,GAAA,CAAI,OAAO,CAAA;AACpC,EAAA,IAAI,UAAU,OAAO,QAAA;AAErB,EAAA,MAAM,OAAA,GAAU,IAAI,OAAA,CAAqB,CAAC,SAAS,MAAA,KAAW;AAC5D,IAAA,MAAM,QAAQ,MAAM;AAClB,MAAA,MAAM,MAAM,MAAA,CAAO,UAAA;AACnB,MAAA,IAAI,CAAC,GAAA,EAAK;AACR,QAAA,MAAA,CAAO,IAAI,KAAA,CAAM,gDAAgD,CAAC,CAAA;AAClE,QAAA;AAAA,MACF;AAGA,MAAA,GAAA,CAAI,KAAA,CAAM,MAAM,OAAA,CAAQ,GAAG,CAAC,CAAA;AAAA,IAC9B,CAAA;AAGA,IAAA,IAAI,OAAO,UAAA,EAAY;AACrB,MAAA,KAAA,EAAM;AACN,MAAA;AAAA,IACF;AAEA,IAAA,MAAM,QAAA,GAAW,CAAA,EAAG,SAAS,CAAA,CAAA,EAAI,OAAO,CAAA,CAAA;AACxC,IAAA,MAAM,KAAA,GAAQ,QAAA,CAAS,cAAA,CAAe,QAAQ,CAAA;AAC9C,IAAA,IAAI,KAAA,EAAO;AACT,MAAA,KAAA,CAAM,iBAAiB,MAAA,EAAQ,KAAA,EAAO,EAAE,IAAA,EAAM,MAAM,CAAA;AACpD,MAAA,KAAA,CAAM,gBAAA;AAAA,QACJ,OAAA;AAAA,QACA,MAAM,MAAA,CAAO,IAAI,KAAA,CAAM,6BAA6B,CAAC,CAAA;AAAA,QACrD,EAAE,MAAM,IAAA;AAAK,OACf;AACA,MAAA;AAAA,IACF;AAEA,IAAA,MAAM,MAAA,GAAS,QAAA,CAAS,aAAA,CAAc,QAAQ,CAAA;AAC9C,IAAA,MAAA,CAAO,EAAA,GAAK,QAAA;AACZ,IAAA,MAAA,CAAO,MAAM,CAAA,EAAG,UAAU,CAAA,EAAG,kBAAA,CAAmB,OAAO,CAAC,CAAA,CAAA;AACxD,IAAA,MAAA,CAAO,KAAA,GAAQ,IAAA;AACf,IAAA,MAAA,CAAO,KAAA,GAAQ,IAAA;AACf,IAAA,MAAA,CAAO,iBAAiB,MAAA,EAAQ,KAAA,EAAO,EAAE,IAAA,EAAM,MAAM,CAAA;AACrD,IAAA,MAAA,CAAO,gBAAA;AAAA,MACL,OAAA;AAAA,MACA,MAAM;AAGJ,QAAA,OAAA,CAAQ,OAAO,OAAO,CAAA;AACtB,QAAA,MAAA,CAAO,IAAI,KAAA,CAAM,6BAA6B,CAAC,CAAA;AAAA,MACjD,CAAA;AAAA,MACA,EAAE,MAAM,IAAA;AAAK,KACf;AACA,IAAA,QAAA,CAAS,IAAA,CAAK,YAAY,MAAM,CAAA;AAAA,EAClC,CAAC,CAAA;AAED,EAAA,OAAA,CAAQ,GAAA,CAAI,SAAS,OAAO,CAAA;AAC5B,EAAA,OAAO,OAAA;AACT;AAQA,eAAsB,mBAAA,CACpB,OAAA,EACA,MAAA,GAAiB,wBAAA,EACA;AACjB,EAAA,MAAM,GAAA,GAAM,MAAM,eAAA,CAAgB,OAAO,CAAA;AACzC,EAAA,OAAO,GAAA,CAAI,OAAA,CAAQ,OAAA,EAAS,EAAE,QAAQ,CAAA;AACxC;;;ACpGO,IAAM,iBAAA,GAAuC,CAAC,UAAU;AAWxD,SAAS,mBAAmB,QAAA,EAA0B;AAC3D,EAAA,MAAM,UAAU,QAAA,CAAS,WAAA,EAAY,CAAE,OAAA,CAAQ,QAAQ,EAAE,CAAA;AACzD,EAAA,OAAO,OAAA,KAAY,KAAK,GAAA,GAAM,OAAA;AAChC;AASO,SAAS,iBAAiB,QAAA,EAA2B;AAC1D,EAAA,OAAO,iBAAA,CAAkB,QAAA,CAAS,kBAAA,CAAmB,QAAQ,CAAC,CAAA;AAChE;AAGO,SAAS,uBAAuB,QAAA,EAA2B;AAChE,EAAA,OAAO,CAAC,iBAAiB,QAAQ,CAAA;AACnC;;;AC3CO,IAAM,uBAAqD,MAAA,CAAO,MAAA;AAAA,EACvE,CAAC,OAAO,MAAA,CAAO,EAAE,QAAQ,QAAA,EAAU,OAAA,EAAS,UAAA,EAAY,CAAC;AAC3D;AAGO,IAAM,uBACX,MAAA,CAAO,MAAA;AAAA,EACL,MAAA,CAAO,WAAA,CAAY,oBAAA,CAAqB,GAAA,CAAI,CAAC,CAAA,KAAM,CAAC,CAAA,CAAE,MAAA,EAAQ,CAAA,CAAE,OAAO,CAAC,CAAC;AAC3E;AASK,IAAM,oBAER,MAAA,CAAO,MAAA,CAAO,EAAE,WAAA,EAAa,WAAW;AAE7C,SAAS,WAAW,IAAA,EAA6C;AAC/D,EAAA,MAAM,MAAA,GAAS,KAAK,WAAA,EAAY;AAChC,EAAA,OAAO,oBAAA,CAAqB,IAAA;AAAA,IAC1B,CAAC,CAAA,KAAM,CAAA,CAAE,MAAA,KAAW,MAAA,IAAU,EAAE,OAAA,KAAY;AAAA,GAC9C;AACF;AAOO,SAAS,qBACd,IAAA,EAC2B;AAC3B,EAAA,IAAI,IAAA,IAAQ,MAAM,OAAO,IAAA;AACzB,EAAA,OAAO,UAAA,CAAW,IAAI,CAAA,EAAG,OAAA,IAAW,IAAA;AACtC;AAGO,SAAS,kBAAkB,IAAA,EAAsB;AACtD,EAAA,OAAO,UAAA,CAAW,IAAI,CAAA,EAAG,MAAA,IAAU,IAAA;AACrC;AAGO,SAAS,oBAAoB,IAAA,EAAwB;AAE1D,EAAA,OAAO;AAAA,IACL,mBAAG,IAAI,GAAA,CAAI,CAAC,IAAA,EAAM,oBAAA,CAAqB,IAAI,CAAA,EAAG,iBAAA,CAAkB,IAAI,CAAC,CAAC;AAAA,GACxE;AACF;AAGO,SAAS,iBAAA,CACd,GACA,CAAA,EACS;AACT,EAAA,IAAI,CAAA,IAAK,IAAA,IAAQ,CAAA,IAAK,IAAA,SAAa,CAAA,KAAM,CAAA;AACzC,EAAA,OAAO,MAAM,CAAA,IAAK,oBAAA,CAAqB,CAAC,CAAA,KAAM,qBAAqB,CAAC,CAAA;AACtE;AAGO,SAAS,gBAAgB,IAAA,EAAsB;AACpD,EAAA,OAAO,kBAAkB,WAAA,KAAgB,SAAA,GACrC,qBAAqB,IAAI,CAAA,GACzB,kBAAkB,IAAI,CAAA;AAC5B","file":"index.js","sourcesContent":["/**\n * Utility function to merge Tailwind CSS classes\n * Combines clsx and tailwind-merge for optimal class handling\n */\n\nimport { type ClassValue, clsx } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\n/**\n * Merge multiple class names intelligently\n * - Handles conditional classes via clsx\n * - Deduplicates and resolves Tailwind conflicts via tailwind-merge\n *\n * @example\n * ```ts\n * cn('px-4 py-2', 'bg-blue-500', { 'text-white': isActive })\n * // => 'px-4 py-2 bg-blue-500 text-white' (if isActive is true)\n * ```\n */\nexport function cn(...inputs: ClassValue[]): string {\n return twMerge(clsx(inputs));\n}\n","/**\n * reCAPTCHA v3 (invisible, score-based) loader and token minter.\n *\n * Deliberately dependency-free: the SDK already ships the `react-google-recaptcha`\n * peer for the **v2 checkbox** used by `ContactPage`, and the two widgets cannot\n * share a script tag — v3 needs `?render=<siteKey>` on the URL, v2 must not have\n * it. This module injects its own v3 script and leaves the v2 integration alone.\n *\n * The Explore chat turns v3 on purely from the backend catalog\n * (`exploreCatalog.captchaRequired && recaptchaSiteKey`), so nothing is loaded\n * and no third-party request is made on sites where captcha is off.\n */\n\n/** The slice of the `grecaptcha` global this module uses. */\nexport interface RecaptchaV3 {\n /** Runs the callback once the API is fully initialised. */\n ready(callback: () => void): void;\n /** Mints a single-use token for `action`. */\n execute(siteKey: string, options: { action: string }): Promise<string>;\n}\n\ndeclare global {\n interface Window {\n grecaptcha?: RecaptchaV3;\n }\n}\n\nconst SCRIPT_ID = \"boff-recaptcha-v3\";\nconst SCRIPT_SRC = \"https://www.google.com/recaptcha/api.js?render=\";\n\n/** Default action name reported to reCAPTCHA for Explore chat submissions. */\nexport const EXPLORE_RECAPTCHA_ACTION = \"explore_chat\";\n\n/**\n * One in-flight/settled promise per site key. Guarantees the script tag is\n * injected at most once even when several components mount at the same time,\n * and lets later callers await the same load instead of racing it.\n */\nconst loaders = new Map<string, Promise<RecaptchaV3>>();\n\nfunction isBrowser(): boolean {\n return typeof window !== \"undefined\" && typeof document !== \"undefined\";\n}\n\n/**\n * Load the reCAPTCHA v3 script for `siteKey` and resolve once `grecaptcha` is\n * ready to mint tokens.\n *\n * - Injects the script at most once per site key (subsequent calls reuse the\n * same promise).\n * - Adopts an already-present script tag (e.g. injected by the host site)\n * rather than adding a second one.\n * - Rejects rather than hanging when the script fails to load, so the caller\n * can decide whether to submit without a token or surface an error.\n */\nexport function loadRecaptchaV3(siteKey: string): Promise<RecaptchaV3> {\n if (!siteKey) {\n return Promise.reject(new Error(\"reCAPTCHA site key is required\"));\n }\n if (!isBrowser()) {\n return Promise.reject(\n new Error(\"reCAPTCHA v3 can only be loaded in a browser\"),\n );\n }\n\n const existing = loaders.get(siteKey);\n if (existing) return existing;\n\n const promise = new Promise<RecaptchaV3>((resolve, reject) => {\n const ready = () => {\n const api = window.grecaptcha;\n if (!api) {\n reject(new Error(\"reCAPTCHA loaded but grecaptcha is unavailable\"));\n return;\n }\n // `ready` queues until the API has finished initialising internally;\n // calling `execute` before that throws.\n api.ready(() => resolve(api));\n };\n\n // Already loaded by this module, another SDK instance, or the host site.\n if (window.grecaptcha) {\n ready();\n return;\n }\n\n const scriptId = `${SCRIPT_ID}-${siteKey}`;\n const found = document.getElementById(scriptId);\n if (found) {\n found.addEventListener(\"load\", ready, { once: true });\n found.addEventListener(\n \"error\",\n () => reject(new Error(\"Failed to load reCAPTCHA v3\")),\n { once: true },\n );\n return;\n }\n\n const script = document.createElement(\"script\");\n script.id = scriptId;\n script.src = `${SCRIPT_SRC}${encodeURIComponent(siteKey)}`;\n script.async = true;\n script.defer = true;\n script.addEventListener(\"load\", ready, { once: true });\n script.addEventListener(\n \"error\",\n () => {\n // Drop the memo so a later attempt can retry a transient CDN failure\n // instead of being stuck with a permanently rejected promise.\n loaders.delete(siteKey);\n reject(new Error(\"Failed to load reCAPTCHA v3\"));\n },\n { once: true },\n );\n document.head.appendChild(script);\n });\n\n loaders.set(siteKey, promise);\n return promise;\n}\n\n/**\n * Mint a single-use reCAPTCHA v3 token, loading the script on first use.\n *\n * Tokens are valid for ~2 minutes and are consumed by one verification, so\n * this must be called per submission — never cached.\n */\nexport async function getRecaptchaV3Token(\n siteKey: string,\n action: string = EXPLORE_RECAPTCHA_ACTION,\n): Promise<string> {\n const api = await loadRecaptchaV3(siteKey);\n return api.execute(siteKey, { action });\n}\n","/**\n * Which routes a host site should render WITHOUT its marketing footer.\n *\n * ## Why this lives in the SDK\n *\n * It was written 39 times. The footer belongs to each website, so the obvious\n * place for the rule is each website — and eight agents asked to add it produced\n * five different helpers (`footerless-routes.ts`, `footer-visibility.ts`,\n * `footer-routes.ts`, `explore-route.ts`, plus six copies inlined straight into\n * a layout). All five behave the same today. The next person to change the rule\n * has to find all five, and will not.\n *\n * The rule is really a property of the SDK's own surfaces: `/explore` is\n * footerless because `ExplorePage` sizes itself to one viewport, which is a fact\n * about this package, not about any site. So it is declared here, once, and the\n * sites ask.\n *\n * ## Why `/explore` and nothing else\n *\n * `ExplorePage` renders a full-viewport chat pane (`100dvh` minus the site\n * chrome above it), so anything after it is below the fold by construction. On a\n * phone these footers are 1100-1330px — roughly 70% of the document — and that\n * bulk is what allowed a restored scroll offset to open the page on nothing but\n * footer, with zero pixels of the chat on screen.\n *\n * The SDK's OTHER pages are the opposite case and must keep their footer:\n * `/concepts`, `/concept/:slug`, `/answers` and `/answer/:slug` are ordinary\n * indexed content pages whose footer carries the site's internal links.\n * Stripping it would cost hundreds of pages their internal linking, and nobody\n * would notice for weeks.\n */\n\n/** Routes that render without the site footer. Exact paths, not prefixes. */\nexport const FOOTERLESS_ROUTES: readonly string[] = [\"/explore\"];\n\n/**\n * Normalise a router pathname for exact comparison.\n *\n * React Router matches case-insensitively (`caseSensitive` defaults to false)\n * and tolerates a trailing slash, so `/Explore/` reaches the same route element\n * as `/explore` and has to be treated the same way here. Half the fleet's\n * hand-written versions compared case-sensitively and would have rendered a\n * footer under the chat pane on `/Explore`.\n */\nexport function normalizeRoutePath(pathname: string): string {\n const trimmed = pathname.toLowerCase().replace(/\\/+$/, \"\");\n return trimmed === \"\" ? \"/\" : trimmed;\n}\n\n/**\n * True when this pathname is one of the SDK's full-screen surfaces.\n *\n * Deliberately an exact-set test. `startsWith(\"/explore\")` would also swallow a\n * future `/explore-beta`, and `includes(\"explore\")` would swallow far more —\n * including `/concept/explore-your-data`, which is a real shape of concept slug.\n */\nexport function isFooterlessPath(pathname: string): boolean {\n return FOOTERLESS_ROUTES.includes(normalizeRoutePath(pathname));\n}\n\n/** Inverse of {@link isFooterlessPath}, for layouts that read better positively. */\nexport function shouldRenderSiteFooter(pathname: string): boolean {\n return !isFooterlessPath(pathname);\n}\n","/**\n * Product slug aliases for products whose machine slug is being renamed\n * (today: `botlit` → `botmanor`, see botmanor-migration PLAN.md Stage 0 / WP-E).\n *\n * - Readers accept both slugs: compare with {@link isSameProductSlug}, look up\n * with {@link productSlugVariants}.\n * - Writers emit {@link emitProductSlug}, which follows {@link PRODUCT_SLUG_EMIT}\n * (`\"current\"` since PLAN.md Stage 5a, so writers emit `botmanor`).\n *\n * Mirrors `@burdenoff/be-sdk/product-alias` (product-slug concern only) so the\n * browser bundle does not pull in the backend SDK.\n */\n\n/** One product whose slug is being renamed. Both values are lowercase. */\nexport interface ProductSlugRename {\n readonly legacy: string;\n readonly current: string;\n}\n\n/** Every product slug rename in flight. */\nexport const PRODUCT_SLUG_RENAMES: readonly ProductSlugRename[] = Object.freeze(\n [Object.freeze({ legacy: \"botlit\", current: \"botmanor\" })],\n);\n\n/** Legacy slug → current slug, e.g. `{ botlit: \"botmanor\" }`. */\nexport const PRODUCT_SLUG_ALIASES: Readonly<Record<string, string>> =\n Object.freeze(\n Object.fromEntries(PRODUCT_SLUG_RENAMES.map((r) => [r.legacy, r.current])),\n );\n\n/** Which spelling a writer emits. */\nexport type ProductSlugGeneration = \"legacy\" | \"current\";\n\n/**\n * Which slug writers emit. Flipped to `\"current\"` at PLAN.md Stage 5a: stored\n * product slugs are migrated to `botmanor`, and every reader accepts both.\n */\nexport const PRODUCT_SLUG_EMIT: Readonly<{\n productSlug: ProductSlugGeneration;\n}> = Object.freeze({ productSlug: \"current\" });\n\nfunction findRename(slug: string): ProductSlugRename | undefined {\n const wanted = slug.toLowerCase();\n return PRODUCT_SLUG_RENAMES.find(\n (r) => r.legacy === wanted || r.current === wanted,\n );\n}\n\n/** `botlit` (any case) → `botmanor`; every other slug is returned unchanged. */\nexport function normalizeProductSlug(slug: string): string;\nexport function normalizeProductSlug(\n slug: string | null | undefined,\n): string | null | undefined;\nexport function normalizeProductSlug(\n slug: string | null | undefined,\n): string | null | undefined {\n if (slug == null) return slug;\n return findRename(slug)?.current ?? slug;\n}\n\n/** Old spelling of a renamed slug (`botmanor` → `botlit`); every other slug unchanged. */\nexport function legacyProductSlug(slug: string): string {\n return findRename(slug)?.legacy ?? slug;\n}\n\n/** Every slug a reader must accept for `slug`, the given one first. */\nexport function productSlugVariants(slug: string): string[] {\n // Canonical lowercase keys come after the given spelling, so \"BotLit\" yields both keys.\n return [\n ...new Set([slug, normalizeProductSlug(slug), legacyProductSlug(slug)]),\n ];\n}\n\n/** True when `a` and `b` are the same product under the rename. */\nexport function isSameProductSlug(\n a: string | null | undefined,\n b: string | null | undefined,\n): boolean {\n if (a == null || b == null) return a === b;\n return a === b || normalizeProductSlug(a) === normalizeProductSlug(b);\n}\n\n/** The slug writers should use today, from either spelling. */\nexport function emitProductSlug(slug: string): string {\n return PRODUCT_SLUG_EMIT.productSlug === \"current\"\n ? normalizeProductSlug(slug)\n : legacyProductSlug(slug);\n}\n"]}
|
package/dist/utils/index.mjs
CHANGED
|
@@ -85,6 +85,40 @@ function shouldRenderSiteFooter(pathname) {
|
|
|
85
85
|
return !isFooterlessPath(pathname);
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
-
|
|
88
|
+
// src/utils/product-slug.ts
|
|
89
|
+
var PRODUCT_SLUG_RENAMES = Object.freeze(
|
|
90
|
+
[Object.freeze({ legacy: "botlit", current: "botmanor" })]
|
|
91
|
+
);
|
|
92
|
+
var PRODUCT_SLUG_ALIASES = Object.freeze(
|
|
93
|
+
Object.fromEntries(PRODUCT_SLUG_RENAMES.map((r) => [r.legacy, r.current]))
|
|
94
|
+
);
|
|
95
|
+
var PRODUCT_SLUG_EMIT = Object.freeze({ productSlug: "current" });
|
|
96
|
+
function findRename(slug) {
|
|
97
|
+
const wanted = slug.toLowerCase();
|
|
98
|
+
return PRODUCT_SLUG_RENAMES.find(
|
|
99
|
+
(r) => r.legacy === wanted || r.current === wanted
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
function normalizeProductSlug(slug) {
|
|
103
|
+
if (slug == null) return slug;
|
|
104
|
+
return findRename(slug)?.current ?? slug;
|
|
105
|
+
}
|
|
106
|
+
function legacyProductSlug(slug) {
|
|
107
|
+
return findRename(slug)?.legacy ?? slug;
|
|
108
|
+
}
|
|
109
|
+
function productSlugVariants(slug) {
|
|
110
|
+
return [
|
|
111
|
+
.../* @__PURE__ */ new Set([slug, normalizeProductSlug(slug), legacyProductSlug(slug)])
|
|
112
|
+
];
|
|
113
|
+
}
|
|
114
|
+
function isSameProductSlug(a, b) {
|
|
115
|
+
if (a == null || b == null) return a === b;
|
|
116
|
+
return a === b || normalizeProductSlug(a) === normalizeProductSlug(b);
|
|
117
|
+
}
|
|
118
|
+
function emitProductSlug(slug) {
|
|
119
|
+
return PRODUCT_SLUG_EMIT.productSlug === "current" ? normalizeProductSlug(slug) : legacyProductSlug(slug);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export { EXPLORE_RECAPTCHA_ACTION, FOOTERLESS_ROUTES, PRODUCT_SLUG_ALIASES, PRODUCT_SLUG_EMIT, PRODUCT_SLUG_RENAMES, cn, emitProductSlug, getRecaptchaV3Token, isFooterlessPath, isSameProductSlug, legacyProductSlug, loadRecaptchaV3, normalizeProductSlug, normalizeRoutePath, productSlugVariants, shouldRenderSiteFooter };
|
|
89
123
|
//# sourceMappingURL=index.mjs.map
|
|
90
124
|
//# sourceMappingURL=index.mjs.map
|
package/dist/utils/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/utils/cn.ts","../../src/utils/recaptcha-v3.ts","../../src/utils/site-chrome.ts"],"names":[],"mappings":"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","file":"index.mjs","sourcesContent":["/**\n * Utility function to merge Tailwind CSS classes\n * Combines clsx and tailwind-merge for optimal class handling\n */\n\nimport { type ClassValue, clsx } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\n/**\n * Merge multiple class names intelligently\n * - Handles conditional classes via clsx\n * - Deduplicates and resolves Tailwind conflicts via tailwind-merge\n *\n * @example\n * ```ts\n * cn('px-4 py-2', 'bg-blue-500', { 'text-white': isActive })\n * // => 'px-4 py-2 bg-blue-500 text-white' (if isActive is true)\n * ```\n */\nexport function cn(...inputs: ClassValue[]): string {\n return twMerge(clsx(inputs));\n}\n","/**\n * reCAPTCHA v3 (invisible, score-based) loader and token minter.\n *\n * Deliberately dependency-free: the SDK already ships the `react-google-recaptcha`\n * peer for the **v2 checkbox** used by `ContactPage`, and the two widgets cannot\n * share a script tag — v3 needs `?render=<siteKey>` on the URL, v2 must not have\n * it. This module injects its own v3 script and leaves the v2 integration alone.\n *\n * The Explore chat turns v3 on purely from the backend catalog\n * (`exploreCatalog.captchaRequired && recaptchaSiteKey`), so nothing is loaded\n * and no third-party request is made on sites where captcha is off.\n */\n\n/** The slice of the `grecaptcha` global this module uses. */\nexport interface RecaptchaV3 {\n /** Runs the callback once the API is fully initialised. */\n ready(callback: () => void): void;\n /** Mints a single-use token for `action`. */\n execute(siteKey: string, options: { action: string }): Promise<string>;\n}\n\ndeclare global {\n interface Window {\n grecaptcha?: RecaptchaV3;\n }\n}\n\nconst SCRIPT_ID = \"boff-recaptcha-v3\";\nconst SCRIPT_SRC = \"https://www.google.com/recaptcha/api.js?render=\";\n\n/** Default action name reported to reCAPTCHA for Explore chat submissions. */\nexport const EXPLORE_RECAPTCHA_ACTION = \"explore_chat\";\n\n/**\n * One in-flight/settled promise per site key. Guarantees the script tag is\n * injected at most once even when several components mount at the same time,\n * and lets later callers await the same load instead of racing it.\n */\nconst loaders = new Map<string, Promise<RecaptchaV3>>();\n\nfunction isBrowser(): boolean {\n return typeof window !== \"undefined\" && typeof document !== \"undefined\";\n}\n\n/**\n * Load the reCAPTCHA v3 script for `siteKey` and resolve once `grecaptcha` is\n * ready to mint tokens.\n *\n * - Injects the script at most once per site key (subsequent calls reuse the\n * same promise).\n * - Adopts an already-present script tag (e.g. injected by the host site)\n * rather than adding a second one.\n * - Rejects rather than hanging when the script fails to load, so the caller\n * can decide whether to submit without a token or surface an error.\n */\nexport function loadRecaptchaV3(siteKey: string): Promise<RecaptchaV3> {\n if (!siteKey) {\n return Promise.reject(new Error(\"reCAPTCHA site key is required\"));\n }\n if (!isBrowser()) {\n return Promise.reject(\n new Error(\"reCAPTCHA v3 can only be loaded in a browser\"),\n );\n }\n\n const existing = loaders.get(siteKey);\n if (existing) return existing;\n\n const promise = new Promise<RecaptchaV3>((resolve, reject) => {\n const ready = () => {\n const api = window.grecaptcha;\n if (!api) {\n reject(new Error(\"reCAPTCHA loaded but grecaptcha is unavailable\"));\n return;\n }\n // `ready` queues until the API has finished initialising internally;\n // calling `execute` before that throws.\n api.ready(() => resolve(api));\n };\n\n // Already loaded by this module, another SDK instance, or the host site.\n if (window.grecaptcha) {\n ready();\n return;\n }\n\n const scriptId = `${SCRIPT_ID}-${siteKey}`;\n const found = document.getElementById(scriptId);\n if (found) {\n found.addEventListener(\"load\", ready, { once: true });\n found.addEventListener(\n \"error\",\n () => reject(new Error(\"Failed to load reCAPTCHA v3\")),\n { once: true },\n );\n return;\n }\n\n const script = document.createElement(\"script\");\n script.id = scriptId;\n script.src = `${SCRIPT_SRC}${encodeURIComponent(siteKey)}`;\n script.async = true;\n script.defer = true;\n script.addEventListener(\"load\", ready, { once: true });\n script.addEventListener(\n \"error\",\n () => {\n // Drop the memo so a later attempt can retry a transient CDN failure\n // instead of being stuck with a permanently rejected promise.\n loaders.delete(siteKey);\n reject(new Error(\"Failed to load reCAPTCHA v3\"));\n },\n { once: true },\n );\n document.head.appendChild(script);\n });\n\n loaders.set(siteKey, promise);\n return promise;\n}\n\n/**\n * Mint a single-use reCAPTCHA v3 token, loading the script on first use.\n *\n * Tokens are valid for ~2 minutes and are consumed by one verification, so\n * this must be called per submission — never cached.\n */\nexport async function getRecaptchaV3Token(\n siteKey: string,\n action: string = EXPLORE_RECAPTCHA_ACTION,\n): Promise<string> {\n const api = await loadRecaptchaV3(siteKey);\n return api.execute(siteKey, { action });\n}\n","/**\n * Which routes a host site should render WITHOUT its marketing footer.\n *\n * ## Why this lives in the SDK\n *\n * It was written 39 times. The footer belongs to each website, so the obvious\n * place for the rule is each website — and eight agents asked to add it produced\n * five different helpers (`footerless-routes.ts`, `footer-visibility.ts`,\n * `footer-routes.ts`, `explore-route.ts`, plus six copies inlined straight into\n * a layout). All five behave the same today. The next person to change the rule\n * has to find all five, and will not.\n *\n * The rule is really a property of the SDK's own surfaces: `/explore` is\n * footerless because `ExplorePage` sizes itself to one viewport, which is a fact\n * about this package, not about any site. So it is declared here, once, and the\n * sites ask.\n *\n * ## Why `/explore` and nothing else\n *\n * `ExplorePage` renders a full-viewport chat pane (`100dvh` minus the site\n * chrome above it), so anything after it is below the fold by construction. On a\n * phone these footers are 1100-1330px — roughly 70% of the document — and that\n * bulk is what allowed a restored scroll offset to open the page on nothing but\n * footer, with zero pixels of the chat on screen.\n *\n * The SDK's OTHER pages are the opposite case and must keep their footer:\n * `/concepts`, `/concept/:slug`, `/answers` and `/answer/:slug` are ordinary\n * indexed content pages whose footer carries the site's internal links.\n * Stripping it would cost hundreds of pages their internal linking, and nobody\n * would notice for weeks.\n */\n\n/** Routes that render without the site footer. Exact paths, not prefixes. */\nexport const FOOTERLESS_ROUTES: readonly string[] = [\"/explore\"];\n\n/**\n * Normalise a router pathname for exact comparison.\n *\n * React Router matches case-insensitively (`caseSensitive` defaults to false)\n * and tolerates a trailing slash, so `/Explore/` reaches the same route element\n * as `/explore` and has to be treated the same way here. Half the fleet's\n * hand-written versions compared case-sensitively and would have rendered a\n * footer under the chat pane on `/Explore`.\n */\nexport function normalizeRoutePath(pathname: string): string {\n const trimmed = pathname.toLowerCase().replace(/\\/+$/, \"\");\n return trimmed === \"\" ? \"/\" : trimmed;\n}\n\n/**\n * True when this pathname is one of the SDK's full-screen surfaces.\n *\n * Deliberately an exact-set test. `startsWith(\"/explore\")` would also swallow a\n * future `/explore-beta`, and `includes(\"explore\")` would swallow far more —\n * including `/concept/explore-your-data`, which is a real shape of concept slug.\n */\nexport function isFooterlessPath(pathname: string): boolean {\n return FOOTERLESS_ROUTES.includes(normalizeRoutePath(pathname));\n}\n\n/** Inverse of {@link isFooterlessPath}, for layouts that read better positively. */\nexport function shouldRenderSiteFooter(pathname: string): boolean {\n return !isFooterlessPath(pathname);\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../../src/utils/cn.ts","../../src/utils/recaptcha-v3.ts","../../src/utils/site-chrome.ts","../../src/utils/product-slug.ts"],"names":[],"mappings":"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","file":"index.mjs","sourcesContent":["/**\n * Utility function to merge Tailwind CSS classes\n * Combines clsx and tailwind-merge for optimal class handling\n */\n\nimport { type ClassValue, clsx } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\n/**\n * Merge multiple class names intelligently\n * - Handles conditional classes via clsx\n * - Deduplicates and resolves Tailwind conflicts via tailwind-merge\n *\n * @example\n * ```ts\n * cn('px-4 py-2', 'bg-blue-500', { 'text-white': isActive })\n * // => 'px-4 py-2 bg-blue-500 text-white' (if isActive is true)\n * ```\n */\nexport function cn(...inputs: ClassValue[]): string {\n return twMerge(clsx(inputs));\n}\n","/**\n * reCAPTCHA v3 (invisible, score-based) loader and token minter.\n *\n * Deliberately dependency-free: the SDK already ships the `react-google-recaptcha`\n * peer for the **v2 checkbox** used by `ContactPage`, and the two widgets cannot\n * share a script tag — v3 needs `?render=<siteKey>` on the URL, v2 must not have\n * it. This module injects its own v3 script and leaves the v2 integration alone.\n *\n * The Explore chat turns v3 on purely from the backend catalog\n * (`exploreCatalog.captchaRequired && recaptchaSiteKey`), so nothing is loaded\n * and no third-party request is made on sites where captcha is off.\n */\n\n/** The slice of the `grecaptcha` global this module uses. */\nexport interface RecaptchaV3 {\n /** Runs the callback once the API is fully initialised. */\n ready(callback: () => void): void;\n /** Mints a single-use token for `action`. */\n execute(siteKey: string, options: { action: string }): Promise<string>;\n}\n\ndeclare global {\n interface Window {\n grecaptcha?: RecaptchaV3;\n }\n}\n\nconst SCRIPT_ID = \"boff-recaptcha-v3\";\nconst SCRIPT_SRC = \"https://www.google.com/recaptcha/api.js?render=\";\n\n/** Default action name reported to reCAPTCHA for Explore chat submissions. */\nexport const EXPLORE_RECAPTCHA_ACTION = \"explore_chat\";\n\n/**\n * One in-flight/settled promise per site key. Guarantees the script tag is\n * injected at most once even when several components mount at the same time,\n * and lets later callers await the same load instead of racing it.\n */\nconst loaders = new Map<string, Promise<RecaptchaV3>>();\n\nfunction isBrowser(): boolean {\n return typeof window !== \"undefined\" && typeof document !== \"undefined\";\n}\n\n/**\n * Load the reCAPTCHA v3 script for `siteKey` and resolve once `grecaptcha` is\n * ready to mint tokens.\n *\n * - Injects the script at most once per site key (subsequent calls reuse the\n * same promise).\n * - Adopts an already-present script tag (e.g. injected by the host site)\n * rather than adding a second one.\n * - Rejects rather than hanging when the script fails to load, so the caller\n * can decide whether to submit without a token or surface an error.\n */\nexport function loadRecaptchaV3(siteKey: string): Promise<RecaptchaV3> {\n if (!siteKey) {\n return Promise.reject(new Error(\"reCAPTCHA site key is required\"));\n }\n if (!isBrowser()) {\n return Promise.reject(\n new Error(\"reCAPTCHA v3 can only be loaded in a browser\"),\n );\n }\n\n const existing = loaders.get(siteKey);\n if (existing) return existing;\n\n const promise = new Promise<RecaptchaV3>((resolve, reject) => {\n const ready = () => {\n const api = window.grecaptcha;\n if (!api) {\n reject(new Error(\"reCAPTCHA loaded but grecaptcha is unavailable\"));\n return;\n }\n // `ready` queues until the API has finished initialising internally;\n // calling `execute` before that throws.\n api.ready(() => resolve(api));\n };\n\n // Already loaded by this module, another SDK instance, or the host site.\n if (window.grecaptcha) {\n ready();\n return;\n }\n\n const scriptId = `${SCRIPT_ID}-${siteKey}`;\n const found = document.getElementById(scriptId);\n if (found) {\n found.addEventListener(\"load\", ready, { once: true });\n found.addEventListener(\n \"error\",\n () => reject(new Error(\"Failed to load reCAPTCHA v3\")),\n { once: true },\n );\n return;\n }\n\n const script = document.createElement(\"script\");\n script.id = scriptId;\n script.src = `${SCRIPT_SRC}${encodeURIComponent(siteKey)}`;\n script.async = true;\n script.defer = true;\n script.addEventListener(\"load\", ready, { once: true });\n script.addEventListener(\n \"error\",\n () => {\n // Drop the memo so a later attempt can retry a transient CDN failure\n // instead of being stuck with a permanently rejected promise.\n loaders.delete(siteKey);\n reject(new Error(\"Failed to load reCAPTCHA v3\"));\n },\n { once: true },\n );\n document.head.appendChild(script);\n });\n\n loaders.set(siteKey, promise);\n return promise;\n}\n\n/**\n * Mint a single-use reCAPTCHA v3 token, loading the script on first use.\n *\n * Tokens are valid for ~2 minutes and are consumed by one verification, so\n * this must be called per submission — never cached.\n */\nexport async function getRecaptchaV3Token(\n siteKey: string,\n action: string = EXPLORE_RECAPTCHA_ACTION,\n): Promise<string> {\n const api = await loadRecaptchaV3(siteKey);\n return api.execute(siteKey, { action });\n}\n","/**\n * Which routes a host site should render WITHOUT its marketing footer.\n *\n * ## Why this lives in the SDK\n *\n * It was written 39 times. The footer belongs to each website, so the obvious\n * place for the rule is each website — and eight agents asked to add it produced\n * five different helpers (`footerless-routes.ts`, `footer-visibility.ts`,\n * `footer-routes.ts`, `explore-route.ts`, plus six copies inlined straight into\n * a layout). All five behave the same today. The next person to change the rule\n * has to find all five, and will not.\n *\n * The rule is really a property of the SDK's own surfaces: `/explore` is\n * footerless because `ExplorePage` sizes itself to one viewport, which is a fact\n * about this package, not about any site. So it is declared here, once, and the\n * sites ask.\n *\n * ## Why `/explore` and nothing else\n *\n * `ExplorePage` renders a full-viewport chat pane (`100dvh` minus the site\n * chrome above it), so anything after it is below the fold by construction. On a\n * phone these footers are 1100-1330px — roughly 70% of the document — and that\n * bulk is what allowed a restored scroll offset to open the page on nothing but\n * footer, with zero pixels of the chat on screen.\n *\n * The SDK's OTHER pages are the opposite case and must keep their footer:\n * `/concepts`, `/concept/:slug`, `/answers` and `/answer/:slug` are ordinary\n * indexed content pages whose footer carries the site's internal links.\n * Stripping it would cost hundreds of pages their internal linking, and nobody\n * would notice for weeks.\n */\n\n/** Routes that render without the site footer. Exact paths, not prefixes. */\nexport const FOOTERLESS_ROUTES: readonly string[] = [\"/explore\"];\n\n/**\n * Normalise a router pathname for exact comparison.\n *\n * React Router matches case-insensitively (`caseSensitive` defaults to false)\n * and tolerates a trailing slash, so `/Explore/` reaches the same route element\n * as `/explore` and has to be treated the same way here. Half the fleet's\n * hand-written versions compared case-sensitively and would have rendered a\n * footer under the chat pane on `/Explore`.\n */\nexport function normalizeRoutePath(pathname: string): string {\n const trimmed = pathname.toLowerCase().replace(/\\/+$/, \"\");\n return trimmed === \"\" ? \"/\" : trimmed;\n}\n\n/**\n * True when this pathname is one of the SDK's full-screen surfaces.\n *\n * Deliberately an exact-set test. `startsWith(\"/explore\")` would also swallow a\n * future `/explore-beta`, and `includes(\"explore\")` would swallow far more —\n * including `/concept/explore-your-data`, which is a real shape of concept slug.\n */\nexport function isFooterlessPath(pathname: string): boolean {\n return FOOTERLESS_ROUTES.includes(normalizeRoutePath(pathname));\n}\n\n/** Inverse of {@link isFooterlessPath}, for layouts that read better positively. */\nexport function shouldRenderSiteFooter(pathname: string): boolean {\n return !isFooterlessPath(pathname);\n}\n","/**\n * Product slug aliases for products whose machine slug is being renamed\n * (today: `botlit` → `botmanor`, see botmanor-migration PLAN.md Stage 0 / WP-E).\n *\n * - Readers accept both slugs: compare with {@link isSameProductSlug}, look up\n * with {@link productSlugVariants}.\n * - Writers emit {@link emitProductSlug}, which follows {@link PRODUCT_SLUG_EMIT}\n * (`\"current\"` since PLAN.md Stage 5a, so writers emit `botmanor`).\n *\n * Mirrors `@burdenoff/be-sdk/product-alias` (product-slug concern only) so the\n * browser bundle does not pull in the backend SDK.\n */\n\n/** One product whose slug is being renamed. Both values are lowercase. */\nexport interface ProductSlugRename {\n readonly legacy: string;\n readonly current: string;\n}\n\n/** Every product slug rename in flight. */\nexport const PRODUCT_SLUG_RENAMES: readonly ProductSlugRename[] = Object.freeze(\n [Object.freeze({ legacy: \"botlit\", current: \"botmanor\" })],\n);\n\n/** Legacy slug → current slug, e.g. `{ botlit: \"botmanor\" }`. */\nexport const PRODUCT_SLUG_ALIASES: Readonly<Record<string, string>> =\n Object.freeze(\n Object.fromEntries(PRODUCT_SLUG_RENAMES.map((r) => [r.legacy, r.current])),\n );\n\n/** Which spelling a writer emits. */\nexport type ProductSlugGeneration = \"legacy\" | \"current\";\n\n/**\n * Which slug writers emit. Flipped to `\"current\"` at PLAN.md Stage 5a: stored\n * product slugs are migrated to `botmanor`, and every reader accepts both.\n */\nexport const PRODUCT_SLUG_EMIT: Readonly<{\n productSlug: ProductSlugGeneration;\n}> = Object.freeze({ productSlug: \"current\" });\n\nfunction findRename(slug: string): ProductSlugRename | undefined {\n const wanted = slug.toLowerCase();\n return PRODUCT_SLUG_RENAMES.find(\n (r) => r.legacy === wanted || r.current === wanted,\n );\n}\n\n/** `botlit` (any case) → `botmanor`; every other slug is returned unchanged. */\nexport function normalizeProductSlug(slug: string): string;\nexport function normalizeProductSlug(\n slug: string | null | undefined,\n): string | null | undefined;\nexport function normalizeProductSlug(\n slug: string | null | undefined,\n): string | null | undefined {\n if (slug == null) return slug;\n return findRename(slug)?.current ?? slug;\n}\n\n/** Old spelling of a renamed slug (`botmanor` → `botlit`); every other slug unchanged. */\nexport function legacyProductSlug(slug: string): string {\n return findRename(slug)?.legacy ?? slug;\n}\n\n/** Every slug a reader must accept for `slug`, the given one first. */\nexport function productSlugVariants(slug: string): string[] {\n // Canonical lowercase keys come after the given spelling, so \"BotLit\" yields both keys.\n return [\n ...new Set([slug, normalizeProductSlug(slug), legacyProductSlug(slug)]),\n ];\n}\n\n/** True when `a` and `b` are the same product under the rename. */\nexport function isSameProductSlug(\n a: string | null | undefined,\n b: string | null | undefined,\n): boolean {\n if (a == null || b == null) return a === b;\n return a === b || normalizeProductSlug(a) === normalizeProductSlug(b);\n}\n\n/** The slug writers should use today, from either spelling. */\nexport function emitProductSlug(slug: string): string {\n return PRODUCT_SLUG_EMIT.productSlug === \"current\"\n ? normalizeProductSlug(slug)\n : legacyProductSlug(slug);\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@burdenoff/website-sdk",
|
|
3
|
-
"version": "2026.
|
|
3
|
+
"version": "2026.927.1",
|
|
4
4
|
"description": "Shared SDK for Burdenoff product websites - reusable React components, utilities, and configurations",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.mjs",
|