@favish/staffbase-utils 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/dist/links.cjs.js +2 -0
  2. package/dist/links.cjs.js.map +1 -0
  3. package/dist/links.es.mjs +98 -0
  4. package/dist/links.es.mjs.map +1 -0
  5. package/dist/renderWidgets-CeIczubt.mjs +81 -0
  6. package/dist/renderWidgets-CeIczubt.mjs.map +1 -0
  7. package/dist/renderWidgets-EbJq9Wn6.js +2 -0
  8. package/dist/renderWidgets-EbJq9Wn6.js.map +1 -0
  9. package/dist/src/links/getInAppOpenLinkTarget.d.ts +11 -0
  10. package/dist/src/links/getInAppOpenLinkTarget.d.ts.map +1 -0
  11. package/dist/src/links/index.d.ts +7 -0
  12. package/dist/src/links/index.d.ts.map +1 -0
  13. package/dist/src/links/isAllowedIframeSrc.d.ts +9 -0
  14. package/dist/src/links/isAllowedIframeSrc.d.ts.map +1 -0
  15. package/dist/src/links/isPromiseLike.d.ts +8 -0
  16. package/dist/src/links/isPromiseLike.d.ts.map +1 -0
  17. package/dist/src/links/isSafeNavigationHref.d.ts +10 -0
  18. package/dist/src/links/isSafeNavigationHref.d.ts.map +1 -0
  19. package/dist/src/links/normalizeInAppLinks.d.ts +14 -0
  20. package/dist/src/links/normalizeInAppLinks.d.ts.map +1 -0
  21. package/dist/src/links/openStaffbaseAware.d.ts +9 -0
  22. package/dist/src/links/openStaffbaseAware.d.ts.map +1 -0
  23. package/dist/src/links/stripStaffbaseLinkPrefix.d.ts +8 -0
  24. package/dist/src/links/stripStaffbaseLinkPrefix.d.ts.map +1 -0
  25. package/dist/src/links/tryOpenWithStaffbase.d.ts +10 -0
  26. package/dist/src/links/tryOpenWithStaffbase.d.ts.map +1 -0
  27. package/dist/src/types/links/TryOpenResult.d.ts +10 -0
  28. package/dist/src/types/links/TryOpenResult.d.ts.map +1 -0
  29. package/dist/src/types/links/WindowWithStaffbase.d.ts +14 -0
  30. package/dist/src/types/links/WindowWithStaffbase.d.ts.map +1 -0
  31. package/dist/src/types/widgets/RenderWidgetsOptions.d.ts +21 -0
  32. package/dist/src/types/widgets/RenderWidgetsOptions.d.ts.map +1 -0
  33. package/dist/src/types/widgets/RenderWidgetsResult.d.ts +13 -0
  34. package/dist/src/types/widgets/RenderWidgetsResult.d.ts.map +1 -0
  35. package/dist/src/types/widgets/StaffbaseWidgetManagerConstructor.d.ts +9 -0
  36. package/dist/src/types/widgets/StaffbaseWidgetManagerConstructor.d.ts.map +1 -0
  37. package/dist/src/types/widgets/StaffbaseWidgetManagerPrototype.d.ts +11 -0
  38. package/dist/src/types/widgets/StaffbaseWidgetManagerPrototype.d.ts.map +1 -0
  39. package/dist/src/types/widgets/UseRenderWidgetsOptions.d.ts +16 -0
  40. package/dist/src/types/widgets/UseRenderWidgetsOptions.d.ts.map +1 -0
  41. package/dist/src/widgets/defaultWidgetOnError.d.ts +10 -0
  42. package/dist/src/widgets/defaultWidgetOnError.d.ts.map +1 -0
  43. package/dist/src/widgets/getWidgetManagerConstructor.d.ts +8 -0
  44. package/dist/src/widgets/getWidgetManagerConstructor.d.ts.map +1 -0
  45. package/dist/src/widgets/getWidgetManagerPrototype.d.ts +8 -0
  46. package/dist/src/widgets/getWidgetManagerPrototype.d.ts.map +1 -0
  47. package/dist/src/widgets/hasRequiredWidgetManagerMethods.d.ts +12 -0
  48. package/dist/src/widgets/hasRequiredWidgetManagerMethods.d.ts.map +1 -0
  49. package/dist/src/widgets/index.d.ts +2 -0
  50. package/dist/src/widgets/index.d.ts.map +1 -0
  51. package/dist/src/widgets/isKnownStaffbaseRenderError.d.ts +9 -0
  52. package/dist/src/widgets/isKnownStaffbaseRenderError.d.ts.map +1 -0
  53. package/dist/src/widgets/react/index.d.ts +2 -0
  54. package/dist/src/widgets/react/index.d.ts.map +1 -0
  55. package/dist/src/widgets/react/useRenderWidgets.d.ts +14 -0
  56. package/dist/src/widgets/react/useRenderWidgets.d.ts.map +1 -0
  57. package/dist/src/widgets/renderWidgets.d.ts +16 -0
  58. package/dist/src/widgets/renderWidgets.d.ts.map +1 -0
  59. package/dist/widgets/react.cjs.js +2 -0
  60. package/dist/widgets/react.cjs.js.map +1 -0
  61. package/dist/widgets/react.es.mjs +28 -0
  62. package/dist/widgets/react.es.mjs.map +1 -0
  63. package/dist/widgets.cjs.js +1 -0
  64. package/dist/widgets.es.mjs +2 -0
  65. package/package.json +17 -1
@@ -0,0 +1,2 @@
1
+ Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});var e=e=>e.startsWith(`/deeplink/`)||e.startsWith(`/openlink/`)?`/${e.slice(10)}`:e,t=(t,n)=>{let r=t?.trim();if(!r||r.startsWith(`#`))return null;let i=r.toLowerCase();if(i.startsWith(`mailto:`)||i.startsWith(`tel:`)||i.startsWith(`sms:`)||i.startsWith(`javascript:`)||i.startsWith(`data:`))return null;try{let t=new URL(n),i=new URL(r,t.toString());if(i.origin===t.origin){let n=e(i.pathname);return new URL(`${n}${i.search}${i.hash}`,t.origin).toString()}return i.toString()}catch{return null}},n=[`youtube.com`,`youtube-nocookie.com`,`youtu.be`,`vimeo.com`,`player.vimeo.com`,`staffbase.com`,`staffbase.rocks`],r=e=>{let t=e.trim();if(!t)return!1;try{let e=typeof window<`u`?window.location.origin:`https://localhost`,r=new URL(t,e);if(r.protocol!==`https:`&&r.protocol!==`http:`)return!1;if(r.origin===e)return!0;let i=r.hostname.toLowerCase();return n.some(e=>i===e||i.endsWith(`.${e}`))}catch{return!1}},i=e=>{let t=e.trim().toLowerCase();return t?!(t.startsWith(`javascript:`)||t.startsWith(`data:`)||t.startsWith(`vbscript:`)):!1},a=(e,t)=>{let n=t?.trim()||``;if(!n)return;let r=Array.from(e.querySelectorAll(`a[href]`));for(let e of r){let t=e.getAttribute(`href`);if(!t)continue;let r=t.trim().toLowerCase();if(!(r.startsWith(`#`)||r.startsWith(`mailto:`)||r.startsWith(`tel:`)||r.startsWith(`sms:`)||r.startsWith(`javascript:`)||r.startsWith(`data:`)))try{let r=new URL(t,n);if(r.origin!==n){e.getAttribute(`target`)===`_blank`&&e.setAttribute(`rel`,`noopener noreferrer`);continue}let i=`${r.pathname}${r.search}${r.hash}`.replace(/^\/deeplink\//,`/`).replace(/^\/openlink\//,`/`);e.setAttribute(`href`,i),e.classList.add(`internal-link`),e.removeAttribute(`target`),e.removeAttribute(`rel`)}catch{}}},o=e=>typeof e==`object`&&!!e&&`then`in e&&typeof e.then==`function`,s=e=>{try{if(typeof window>`u`)return{handled:!1};let t=window?.staffbase?.plugin?.util?.openLink;if(typeof t!=`function`)return{handled:!1};let n=t(e,{}),r;return n&&o(n)&&(r=n),{handled:!0,promise:r}}catch{return{handled:!1}}},c=e=>{if(!e||!i(e))return!1;let t=s(e);if(t.handled){try{let n=typeof window<`u`?window.location.href:void 0,r=setTimeout(()=>{try{let t=typeof window<`u`?window.location.href:void 0,r=typeof document<`u`?document.visibilityState:void 0;n&&t&&n===t&&r===`visible`&&typeof window<`u`&&window.location.assign(e)}catch{}},400);try{t.promise?.then(()=>{clearTimeout(r)}).catch(()=>{})}catch{}}catch{}return!0}try{typeof window<`u`&&window.location.assign(e)}catch{}return!1};exports.getInAppOpenLinkTarget=t,exports.isAllowedIframeSrc=r,exports.isSafeNavigationHref=i,exports.normalizeInAppLinks=a,exports.openStaffbaseAware=c,exports.tryOpenWithStaffbase=s;
2
+ //# sourceMappingURL=links.cjs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"links.cjs.js","names":[],"sources":["../src/links/stripStaffbaseLinkPrefix.ts","../src/links/getInAppOpenLinkTarget.ts","../src/links/isAllowedIframeSrc.ts","../src/links/isSafeNavigationHref.ts","../src/links/normalizeInAppLinks.ts","../src/links/isPromiseLike.ts","../src/links/tryOpenWithStaffbase.ts","../src/links/openStaffbaseAware.ts"],"sourcesContent":["/**\n * Strips a leading Staffbase link prefix (`/deeplink/` or `/openlink/`) from a\n * URL pathname, leaving the canonical in-app path.\n * @param {string} path - The URL pathname.\n * @returns {string} The path without the Staffbase prefix.\n */\nexport const stripStaffbaseLinkPrefix = (path: string): string => {\n if (path.startsWith('/deeplink/'))\n return `/${path.slice('/deeplink/'.length)}`\n if (path.startsWith('/openlink/'))\n return `/${path.slice('/openlink/'.length)}`\n return path\n}\n","import { stripStaffbaseLinkPrefix } from './stripStaffbaseLinkPrefix'\n\n/**\n * Returns an absolute URL string that is safe to pass into Staffbase's\n * `openLink()`. We intentionally do NOT require `/openlink/`; if the platform\n * strips it, links can still open in-app by delegating to `openLink()`. Ported\n * from staffbase-alerts (identical to unack).\n * @param {string | null | undefined} href - Raw href as found on an <a> element.\n * @param {string} baseUrl - Base URL used to resolve relative hrefs (usually window.location.origin).\n * @returns {string | null} An absolute URL string, or null if it should not be handled.\n */\nexport const getInAppOpenLinkTarget = (\n href: string | null | undefined,\n baseUrl: string,\n): string | null => {\n const trimmed = href?.trim()\n if (!trimmed) return null\n\n // Ignore anchors and non-navigational links.\n if (trimmed.startsWith('#')) return null\n\n // Let the browser handle special protocols.\n const lower = trimmed.toLowerCase()\n if (\n lower.startsWith('mailto:') ||\n lower.startsWith('tel:') ||\n lower.startsWith('sms:') ||\n lower.startsWith('javascript:') ||\n lower.startsWith('data:')\n ) {\n return null\n }\n\n try {\n const base = new URL(baseUrl)\n const url = new URL(trimmed, base.toString())\n\n // Canonicalize same-origin links by stripping Staffbase prefixes, but keep them absolute.\n if (url.origin === base.origin) {\n const cleanedPath = stripStaffbaseLinkPrefix(url.pathname)\n const absolute = new URL(\n `${cleanedPath}${url.search}${url.hash}`,\n base.origin,\n )\n return absolute.toString()\n }\n\n // For cross-origin absolute URLs, return as-is.\n return url.toString()\n } catch {\n return null\n }\n}\n","// Hosts allowed to be framed inside sanitized article content. Article HTML is\n// first-party authored in Staffbase, but it renders inside a session-bearing\n// webview, so iframe sources are constrained to known embed providers plus the\n// Staffbase origin rather than allowing arbitrary framing. Promoted from\n// staffbase-alerts.\nconst ALLOWED_IFRAME_HOST_SUFFIXES = [\n 'youtube.com',\n 'youtube-nocookie.com',\n 'youtu.be',\n 'vimeo.com',\n 'player.vimeo.com',\n 'staffbase.com',\n 'staffbase.rocks',\n]\n\n/**\n * Returns true when an iframe src may be rendered. Same-origin sources are\n * always allowed; cross-origin sources must match the embed allowlist. Pair with\n * sanitizeArticleHtml's `isAllowedIframeSrc` option.\n * @param {string} src - The iframe src attribute value.\n * @returns {boolean} True when the iframe may be kept.\n */\nexport const isAllowedIframeSrc = (src: string): boolean => {\n const trimmed = src.trim()\n if (!trimmed) return false\n\n try {\n const origin =\n typeof window !== 'undefined'\n ? window.location.origin\n : 'https://localhost'\n const url = new URL(trimmed, origin)\n\n if (url.protocol !== 'https:' && url.protocol !== 'http:') return false\n if (url.origin === origin) return true\n\n const host = url.hostname.toLowerCase()\n return ALLOWED_IFRAME_HOST_SUFFIXES.some(\n (suffix) => host === suffix || host.endsWith(`.${suffix}`),\n )\n } catch {\n return false\n }\n}\n","/**\n * Returns true when an href is safe to pass to a real browser navigation\n * (e.g. `window.location.assign`). Blocks scripted/inline schemes that could\n * execute in the session-bearing webview. Relative URLs and http(s)/mailto/tel\n * are allowed. Promoted from staffbase-alerts.\n * @param {string} href - The href to validate.\n * @returns {boolean} True when the href is safe to navigate to.\n */\nexport const isSafeNavigationHref = (href: string): boolean => {\n const trimmed = href.trim().toLowerCase()\n if (!trimmed) return false\n\n return !(\n trimmed.startsWith('javascript:') ||\n trimmed.startsWith('data:') ||\n trimmed.startsWith('vbscript:')\n )\n}\n","/**\n * Normalizes links inside Staffbase-rendered HTML so they behave better in mobile\n * apps. On iOS, absolute same-origin links and/or `target=\"_blank\"` can trigger\n * Safari instead of in-app navigation; for same-origin links we rewrite to\n * relative paths and remove target/rel. Ported from staffbase-alerts (the\n * superset): cross-origin `target=\"_blank\"` links are hardened with\n * `rel=\"noopener noreferrer\"` instead of left untouched. The Staffbase origin is\n * passed in (the lib never reads env).\n * @param {HTMLElement} root - The container whose anchors are normalized.\n * @param {string} staffbaseOrigin - The Staffbase origin used to detect same-origin links.\n * @returns {void}\n */\nexport const normalizeInAppLinks = (\n root: HTMLElement,\n staffbaseOrigin: string,\n): void => {\n const origin = staffbaseOrigin?.trim() || ''\n if (!origin) return\n\n const anchors = Array.from(root.querySelectorAll('a[href]'))\n\n for (const anchor of anchors) {\n const rawHref = anchor.getAttribute('href')\n if (!rawHref) continue\n\n // Skip special protocols and anchors.\n const lower = rawHref.trim().toLowerCase()\n if (\n lower.startsWith('#') ||\n lower.startsWith('mailto:') ||\n lower.startsWith('tel:') ||\n lower.startsWith('sms:') ||\n lower.startsWith('javascript:') ||\n lower.startsWith('data:')\n ) {\n continue\n }\n\n try {\n const url = new URL(rawHref, origin)\n\n // Only rewrite links that point to the same Staffbase origin.\n if (url.origin !== origin) {\n // Harden cross-origin new-tab links against reverse tabnabbing rather\n // than leaving the authored target/rel untouched.\n if (anchor.getAttribute('target') === '_blank') {\n anchor.setAttribute('rel', 'noopener noreferrer')\n }\n continue\n }\n\n // Convert to a relative URL so the mobile app treats it as in-app navigation.\n const relative = `${url.pathname}${url.search}${url.hash}`\n const withoutStaffbasePrefix = relative\n .replace(/^\\/deeplink\\//, '/')\n .replace(/^\\/openlink\\//, '/')\n anchor.setAttribute('href', withoutStaffbasePrefix)\n\n // Staffbase uses this class in various contexts to mark links as internal.\n // Adding it here increases the chance that the iOS app routes the navigation in-app.\n anchor.classList.add('internal-link')\n\n // Avoid iOS opening Safari for \"new window\" navigation.\n anchor.removeAttribute('target')\n anchor.removeAttribute('rel')\n } catch {\n // If parsing fails, leave link as-is.\n }\n }\n}\n","/**\n * Type guard for Promise-like (thenable) values, used to normalize whatever\n * Staffbase's openLink returns.\n * @param {unknown} value - The value to test.\n * @returns {boolean} True when value has a callable then method.\n */\nexport const isPromiseLike = (value: unknown): value is PromiseLike<unknown> =>\n value !== null &&\n typeof value === 'object' &&\n 'then' in value &&\n typeof (value as PromiseLike<unknown>).then === 'function'\n","import type { TryOpenResult } from '../types/links/TryOpenResult'\nimport type { WindowWithStaffbase } from '../types/links/WindowWithStaffbase'\n\nimport { isPromiseLike } from './isPromiseLike'\n\n/**\n * Attempt to open a link using Staffbase's plugin util.openLink if available.\n * Ported from staffbase-alerts (typed Promise-like guard; smart-search used\n * `as any`).\n * @param {string} href - The target URL to open.\n * @returns {TryOpenResult} Whether Staffbase handled it and a promise if available.\n */\nexport const tryOpenWithStaffbase = (href: string): TryOpenResult => {\n try {\n if (typeof window === 'undefined') {\n return { handled: false }\n }\n const w = window as WindowWithStaffbase\n const open = w?.staffbase?.plugin?.util?.openLink\n const hasOpenLink = typeof open === 'function'\n if (!hasOpenLink) return { handled: false }\n // Pass empty options as the second parameter to align with openLink signature\n const maybePromise = open(href, {})\n // If a Promise is returned, normalize it\n let normalizedPromise: Promise<void> | undefined\n if (maybePromise && isPromiseLike(maybePromise)) {\n normalizedPromise = maybePromise as Promise<void>\n }\n return { handled: true, promise: normalizedPromise }\n } catch {\n return { handled: false }\n }\n}\n","import { isSafeNavigationHref } from './isSafeNavigationHref'\nimport { tryOpenWithStaffbase } from './tryOpenWithStaffbase'\n\n/**\n * Try to open with Staffbase; if not available, navigate normally in the same\n * tab. Ported from staffbase-alerts (the superset): it guards with\n * isSafeNavigationHref first, so scripted schemes never reach navigation.\n * @param {string} href - The target URL to open.\n * @returns {boolean} True if Staffbase handled the navigation, otherwise false.\n */\nexport const openStaffbaseAware = (href: string): boolean => {\n if (!href) return false\n\n // Never navigate to scripted/inline schemes (javascript:, data:, vbscript:),\n // which would execute in the session-bearing webview.\n if (!isSafeNavigationHref(href)) return false\n\n const result = tryOpenWithStaffbase(href)\n if (result.handled) {\n // Watchdog: if Staffbase openLink is a no-op, force navigation shortly after\n try {\n const beforeHref =\n typeof window !== 'undefined' ? window.location.href : undefined\n const timer = setTimeout(() => {\n try {\n const afterHref =\n typeof window !== 'undefined' ? window.location.href : undefined\n const visibility =\n typeof document !== 'undefined'\n ? document.visibilityState\n : undefined\n const noChange = beforeHref && afterHref && beforeHref === afterHref\n const stillVisible = visibility === 'visible'\n if (noChange && stillVisible && typeof window !== 'undefined') {\n window.location.assign(href)\n }\n } catch {\n // Ignore watchdog navigation failures.\n }\n }, 400)\n // If the Staffbase promise resolves, cancel the watchdog fallback\n try {\n result.promise\n ?.then(() => {\n clearTimeout(timer)\n })\n .catch(() => {\n // Keep the watchdog active on rejection.\n })\n } catch {\n // Ignore promise-wiring failures.\n }\n } catch {\n // Ignore watchdog setup failures.\n }\n return true\n }\n try {\n if (typeof window !== 'undefined') {\n window.location.assign(href)\n }\n } catch {\n // Ignore navigation failures.\n }\n return false\n}\n"],"mappings":"mEAMA,IAAa,EAA4B,GACnC,EAAK,WAAW,YAAY,GAE5B,EAAK,WAAW,YAAY,EACvB,IAAI,EAAK,MAAM,EAAmB,IACpC,ECAI,GACX,EACA,IACkB,CAClB,IAAM,EAAU,GAAM,KAAK,EAI3B,GAHI,CAAC,GAGD,EAAQ,WAAW,GAAG,EAAG,OAAO,KAGpC,IAAM,EAAQ,EAAQ,YAAY,EAClC,GACE,EAAM,WAAW,SAAS,GAC1B,EAAM,WAAW,MAAM,GACvB,EAAM,WAAW,MAAM,GACvB,EAAM,WAAW,aAAa,GAC9B,EAAM,WAAW,OAAO,EAExB,OAAO,KAGT,GAAI,CACF,IAAM,EAAO,IAAI,IAAI,CAAO,EACtB,EAAM,IAAI,IAAI,EAAS,EAAK,SAAS,CAAC,EAG5C,GAAI,EAAI,SAAW,EAAK,OAAQ,CAC9B,IAAM,EAAc,EAAyB,EAAI,QAAQ,EAKzD,OAAO,IAJc,IACnB,GAAG,IAAc,EAAI,SAAS,EAAI,OAClC,EAAK,MAEA,EAAS,SAAS,CAC3B,CAGA,OAAO,EAAI,SAAS,CACtB,MAAQ,CACN,OAAO,IACT,CACF,EC/CM,EAA+B,CACnC,cACA,uBACA,WACA,YACA,mBACA,gBACA,iBACF,EASa,EAAsB,GAAyB,CAC1D,IAAM,EAAU,EAAI,KAAK,EACzB,GAAI,CAAC,EAAS,MAAO,GAErB,GAAI,CACF,IAAM,EACJ,OAAO,OAAW,IACd,OAAO,SAAS,OAChB,oBACA,EAAM,IAAI,IAAI,EAAS,CAAM,EAEnC,GAAI,EAAI,WAAa,UAAY,EAAI,WAAa,QAAS,MAAO,GAClE,GAAI,EAAI,SAAW,EAAQ,MAAO,GAElC,IAAM,EAAO,EAAI,SAAS,YAAY,EACtC,OAAO,EAA6B,KACjC,GAAW,IAAS,GAAU,EAAK,SAAS,IAAI,GAAQ,CAC3D,CACF,MAAQ,CACN,MAAO,EACT,CACF,ECnCa,EAAwB,GAA0B,CAC7D,IAAM,EAAU,EAAK,KAAK,EAAE,YAAY,EAGxC,OAFK,EAEE,EACL,EAAQ,WAAW,aAAa,GAChC,EAAQ,WAAW,OAAO,GAC1B,EAAQ,WAAW,WAAW,GALX,EAOvB,ECLa,GACX,EACA,IACS,CACT,IAAM,EAAS,GAAiB,KAAK,GAAK,GAC1C,GAAI,CAAC,EAAQ,OAEb,IAAM,EAAU,MAAM,KAAK,EAAK,iBAAiB,SAAS,CAAC,EAE3D,IAAK,IAAM,KAAU,EAAS,CAC5B,IAAM,EAAU,EAAO,aAAa,MAAM,EAC1C,GAAI,CAAC,EAAS,SAGd,IAAM,EAAQ,EAAQ,KAAK,EAAE,YAAY,EAEvC,OAAM,WAAW,GAAG,GACpB,EAAM,WAAW,SAAS,GAC1B,EAAM,WAAW,MAAM,GACvB,EAAM,WAAW,MAAM,GACvB,EAAM,WAAW,aAAa,GAC9B,EAAM,WAAW,OAAO,GAK1B,GAAI,CACF,IAAM,EAAM,IAAI,IAAI,EAAS,CAAM,EAGnC,GAAI,EAAI,SAAW,EAAQ,CAGrB,EAAO,aAAa,QAAQ,IAAM,UACpC,EAAO,aAAa,MAAO,qBAAqB,EAElD,QACF,CAIA,IAAM,EAAyB,GADX,EAAI,WAAW,EAAI,SAAS,EAAI,OAEjD,QAAQ,gBAAiB,GAAG,EAC5B,QAAQ,gBAAiB,GAAG,EAC/B,EAAO,aAAa,OAAQ,CAAsB,EAIlD,EAAO,UAAU,IAAI,eAAe,EAGpC,EAAO,gBAAgB,QAAQ,EAC/B,EAAO,gBAAgB,KAAK,CAC9B,MAAQ,CAER,CACF,CACF,EC/Da,EAAiB,GAE5B,OAAO,GAAU,YADjB,GAEA,SAAU,GACV,OAAQ,EAA+B,MAAS,WCErC,EAAwB,GAAgC,CACnE,GAAI,CACF,GAAI,OAAO,OAAW,IACpB,MAAO,CAAE,QAAS,EAAM,EAG1B,IAAM,EAAO,QAAG,WAAW,QAAQ,MAAM,SAEzC,GADoB,OAAO,GAAS,WAClB,MAAO,CAAE,QAAS,EAAM,EAE1C,IAAM,EAAe,EAAK,EAAM,CAAC,CAAC,EAE9B,EAIJ,OAHI,GAAgB,EAAc,CAAY,IAC5C,EAAoB,GAEf,CAAE,QAAS,GAAM,QAAS,CAAkB,CACrD,MAAQ,CACN,MAAO,CAAE,QAAS,EAAM,CAC1B,CACF,ECtBa,EAAsB,GAA0B,CAK3D,GAJI,CAAC,GAID,CAAC,EAAqB,CAAI,EAAG,MAAO,GAExC,IAAM,EAAS,EAAqB,CAAI,EACxC,GAAI,EAAO,QAAS,CAElB,GAAI,CACF,IAAM,EACJ,OAAO,OAAW,IAAc,OAAO,SAAS,KAAO,IAAA,GACnD,EAAQ,eAAiB,CAC7B,GAAI,CACF,IAAM,EACJ,OAAO,OAAW,IAAc,OAAO,SAAS,KAAO,IAAA,GACnD,EACJ,OAAO,SAAa,IAChB,SAAS,gBACT,IAAA,GACW,GAAc,GAAa,IAAe,GACtC,IAAe,WACJ,OAAO,OAAW,KAChD,OAAO,SAAS,OAAO,CAAI,CAE/B,MAAQ,CAER,CACF,EAAG,GAAG,EAEN,GAAI,CACF,EAAO,SACH,SAAW,CACX,aAAa,CAAK,CACpB,CAAC,EACA,UAAY,CAEb,CAAC,CACL,MAAQ,CAER,CACF,MAAQ,CAER,CACA,MAAO,EACT,CACA,GAAI,CACE,OAAO,OAAW,KACpB,OAAO,SAAS,OAAO,CAAI,CAE/B,MAAQ,CAER,CACA,MAAO,EACT"}
@@ -0,0 +1,98 @@
1
+ //#region src/links/stripStaffbaseLinkPrefix.ts
2
+ var e = (e) => e.startsWith("/deeplink/") || e.startsWith("/openlink/") ? `/${e.slice(10)}` : e, t = (t, n) => {
3
+ let r = t?.trim();
4
+ if (!r || r.startsWith("#")) return null;
5
+ let i = r.toLowerCase();
6
+ if (i.startsWith("mailto:") || i.startsWith("tel:") || i.startsWith("sms:") || i.startsWith("javascript:") || i.startsWith("data:")) return null;
7
+ try {
8
+ let t = new URL(n), i = new URL(r, t.toString());
9
+ if (i.origin === t.origin) {
10
+ let n = e(i.pathname);
11
+ return new URL(`${n}${i.search}${i.hash}`, t.origin).toString();
12
+ }
13
+ return i.toString();
14
+ } catch {
15
+ return null;
16
+ }
17
+ }, n = [
18
+ "youtube.com",
19
+ "youtube-nocookie.com",
20
+ "youtu.be",
21
+ "vimeo.com",
22
+ "player.vimeo.com",
23
+ "staffbase.com",
24
+ "staffbase.rocks"
25
+ ], r = (e) => {
26
+ let t = e.trim();
27
+ if (!t) return !1;
28
+ try {
29
+ let e = typeof window < "u" ? window.location.origin : "https://localhost", r = new URL(t, e);
30
+ if (r.protocol !== "https:" && r.protocol !== "http:") return !1;
31
+ if (r.origin === e) return !0;
32
+ let i = r.hostname.toLowerCase();
33
+ return n.some((e) => i === e || i.endsWith(`.${e}`));
34
+ } catch {
35
+ return !1;
36
+ }
37
+ }, i = (e) => {
38
+ let t = e.trim().toLowerCase();
39
+ return t ? !(t.startsWith("javascript:") || t.startsWith("data:") || t.startsWith("vbscript:")) : !1;
40
+ }, a = (e, t) => {
41
+ let n = t?.trim() || "";
42
+ if (!n) return;
43
+ let r = Array.from(e.querySelectorAll("a[href]"));
44
+ for (let e of r) {
45
+ let t = e.getAttribute("href");
46
+ if (!t) continue;
47
+ let r = t.trim().toLowerCase();
48
+ if (!(r.startsWith("#") || r.startsWith("mailto:") || r.startsWith("tel:") || r.startsWith("sms:") || r.startsWith("javascript:") || r.startsWith("data:"))) try {
49
+ let r = new URL(t, n);
50
+ if (r.origin !== n) {
51
+ e.getAttribute("target") === "_blank" && e.setAttribute("rel", "noopener noreferrer");
52
+ continue;
53
+ }
54
+ let i = `${r.pathname}${r.search}${r.hash}`.replace(/^\/deeplink\//, "/").replace(/^\/openlink\//, "/");
55
+ e.setAttribute("href", i), e.classList.add("internal-link"), e.removeAttribute("target"), e.removeAttribute("rel");
56
+ } catch {}
57
+ }
58
+ }, o = (e) => typeof e == "object" && !!e && "then" in e && typeof e.then == "function", s = (e) => {
59
+ try {
60
+ if (typeof window > "u") return { handled: !1 };
61
+ let t = window?.staffbase?.plugin?.util?.openLink;
62
+ if (typeof t != "function") return { handled: !1 };
63
+ let n = t(e, {}), r;
64
+ return n && o(n) && (r = n), {
65
+ handled: !0,
66
+ promise: r
67
+ };
68
+ } catch {
69
+ return { handled: !1 };
70
+ }
71
+ }, c = (e) => {
72
+ if (!e || !i(e)) return !1;
73
+ let t = s(e);
74
+ if (t.handled) {
75
+ try {
76
+ let n = typeof window < "u" ? window.location.href : void 0, r = setTimeout(() => {
77
+ try {
78
+ let t = typeof window < "u" ? window.location.href : void 0, r = typeof document < "u" ? document.visibilityState : void 0;
79
+ n && t && n === t && r === "visible" && typeof window < "u" && window.location.assign(e);
80
+ } catch {}
81
+ }, 400);
82
+ try {
83
+ t.promise?.then(() => {
84
+ clearTimeout(r);
85
+ }).catch(() => {});
86
+ } catch {}
87
+ } catch {}
88
+ return !0;
89
+ }
90
+ try {
91
+ typeof window < "u" && window.location.assign(e);
92
+ } catch {}
93
+ return !1;
94
+ };
95
+ //#endregion
96
+ export { t as getInAppOpenLinkTarget, r as isAllowedIframeSrc, i as isSafeNavigationHref, a as normalizeInAppLinks, c as openStaffbaseAware, s as tryOpenWithStaffbase };
97
+
98
+ //# sourceMappingURL=links.es.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"links.es.mjs","names":[],"sources":["../src/links/stripStaffbaseLinkPrefix.ts","../src/links/getInAppOpenLinkTarget.ts","../src/links/isAllowedIframeSrc.ts","../src/links/isSafeNavigationHref.ts","../src/links/normalizeInAppLinks.ts","../src/links/isPromiseLike.ts","../src/links/tryOpenWithStaffbase.ts","../src/links/openStaffbaseAware.ts"],"sourcesContent":["/**\n * Strips a leading Staffbase link prefix (`/deeplink/` or `/openlink/`) from a\n * URL pathname, leaving the canonical in-app path.\n * @param {string} path - The URL pathname.\n * @returns {string} The path without the Staffbase prefix.\n */\nexport const stripStaffbaseLinkPrefix = (path: string): string => {\n if (path.startsWith('/deeplink/'))\n return `/${path.slice('/deeplink/'.length)}`\n if (path.startsWith('/openlink/'))\n return `/${path.slice('/openlink/'.length)}`\n return path\n}\n","import { stripStaffbaseLinkPrefix } from './stripStaffbaseLinkPrefix'\n\n/**\n * Returns an absolute URL string that is safe to pass into Staffbase's\n * `openLink()`. We intentionally do NOT require `/openlink/`; if the platform\n * strips it, links can still open in-app by delegating to `openLink()`. Ported\n * from staffbase-alerts (identical to unack).\n * @param {string | null | undefined} href - Raw href as found on an <a> element.\n * @param {string} baseUrl - Base URL used to resolve relative hrefs (usually window.location.origin).\n * @returns {string | null} An absolute URL string, or null if it should not be handled.\n */\nexport const getInAppOpenLinkTarget = (\n href: string | null | undefined,\n baseUrl: string,\n): string | null => {\n const trimmed = href?.trim()\n if (!trimmed) return null\n\n // Ignore anchors and non-navigational links.\n if (trimmed.startsWith('#')) return null\n\n // Let the browser handle special protocols.\n const lower = trimmed.toLowerCase()\n if (\n lower.startsWith('mailto:') ||\n lower.startsWith('tel:') ||\n lower.startsWith('sms:') ||\n lower.startsWith('javascript:') ||\n lower.startsWith('data:')\n ) {\n return null\n }\n\n try {\n const base = new URL(baseUrl)\n const url = new URL(trimmed, base.toString())\n\n // Canonicalize same-origin links by stripping Staffbase prefixes, but keep them absolute.\n if (url.origin === base.origin) {\n const cleanedPath = stripStaffbaseLinkPrefix(url.pathname)\n const absolute = new URL(\n `${cleanedPath}${url.search}${url.hash}`,\n base.origin,\n )\n return absolute.toString()\n }\n\n // For cross-origin absolute URLs, return as-is.\n return url.toString()\n } catch {\n return null\n }\n}\n","// Hosts allowed to be framed inside sanitized article content. Article HTML is\n// first-party authored in Staffbase, but it renders inside a session-bearing\n// webview, so iframe sources are constrained to known embed providers plus the\n// Staffbase origin rather than allowing arbitrary framing. Promoted from\n// staffbase-alerts.\nconst ALLOWED_IFRAME_HOST_SUFFIXES = [\n 'youtube.com',\n 'youtube-nocookie.com',\n 'youtu.be',\n 'vimeo.com',\n 'player.vimeo.com',\n 'staffbase.com',\n 'staffbase.rocks',\n]\n\n/**\n * Returns true when an iframe src may be rendered. Same-origin sources are\n * always allowed; cross-origin sources must match the embed allowlist. Pair with\n * sanitizeArticleHtml's `isAllowedIframeSrc` option.\n * @param {string} src - The iframe src attribute value.\n * @returns {boolean} True when the iframe may be kept.\n */\nexport const isAllowedIframeSrc = (src: string): boolean => {\n const trimmed = src.trim()\n if (!trimmed) return false\n\n try {\n const origin =\n typeof window !== 'undefined'\n ? window.location.origin\n : 'https://localhost'\n const url = new URL(trimmed, origin)\n\n if (url.protocol !== 'https:' && url.protocol !== 'http:') return false\n if (url.origin === origin) return true\n\n const host = url.hostname.toLowerCase()\n return ALLOWED_IFRAME_HOST_SUFFIXES.some(\n (suffix) => host === suffix || host.endsWith(`.${suffix}`),\n )\n } catch {\n return false\n }\n}\n","/**\n * Returns true when an href is safe to pass to a real browser navigation\n * (e.g. `window.location.assign`). Blocks scripted/inline schemes that could\n * execute in the session-bearing webview. Relative URLs and http(s)/mailto/tel\n * are allowed. Promoted from staffbase-alerts.\n * @param {string} href - The href to validate.\n * @returns {boolean} True when the href is safe to navigate to.\n */\nexport const isSafeNavigationHref = (href: string): boolean => {\n const trimmed = href.trim().toLowerCase()\n if (!trimmed) return false\n\n return !(\n trimmed.startsWith('javascript:') ||\n trimmed.startsWith('data:') ||\n trimmed.startsWith('vbscript:')\n )\n}\n","/**\n * Normalizes links inside Staffbase-rendered HTML so they behave better in mobile\n * apps. On iOS, absolute same-origin links and/or `target=\"_blank\"` can trigger\n * Safari instead of in-app navigation; for same-origin links we rewrite to\n * relative paths and remove target/rel. Ported from staffbase-alerts (the\n * superset): cross-origin `target=\"_blank\"` links are hardened with\n * `rel=\"noopener noreferrer\"` instead of left untouched. The Staffbase origin is\n * passed in (the lib never reads env).\n * @param {HTMLElement} root - The container whose anchors are normalized.\n * @param {string} staffbaseOrigin - The Staffbase origin used to detect same-origin links.\n * @returns {void}\n */\nexport const normalizeInAppLinks = (\n root: HTMLElement,\n staffbaseOrigin: string,\n): void => {\n const origin = staffbaseOrigin?.trim() || ''\n if (!origin) return\n\n const anchors = Array.from(root.querySelectorAll('a[href]'))\n\n for (const anchor of anchors) {\n const rawHref = anchor.getAttribute('href')\n if (!rawHref) continue\n\n // Skip special protocols and anchors.\n const lower = rawHref.trim().toLowerCase()\n if (\n lower.startsWith('#') ||\n lower.startsWith('mailto:') ||\n lower.startsWith('tel:') ||\n lower.startsWith('sms:') ||\n lower.startsWith('javascript:') ||\n lower.startsWith('data:')\n ) {\n continue\n }\n\n try {\n const url = new URL(rawHref, origin)\n\n // Only rewrite links that point to the same Staffbase origin.\n if (url.origin !== origin) {\n // Harden cross-origin new-tab links against reverse tabnabbing rather\n // than leaving the authored target/rel untouched.\n if (anchor.getAttribute('target') === '_blank') {\n anchor.setAttribute('rel', 'noopener noreferrer')\n }\n continue\n }\n\n // Convert to a relative URL so the mobile app treats it as in-app navigation.\n const relative = `${url.pathname}${url.search}${url.hash}`\n const withoutStaffbasePrefix = relative\n .replace(/^\\/deeplink\\//, '/')\n .replace(/^\\/openlink\\//, '/')\n anchor.setAttribute('href', withoutStaffbasePrefix)\n\n // Staffbase uses this class in various contexts to mark links as internal.\n // Adding it here increases the chance that the iOS app routes the navigation in-app.\n anchor.classList.add('internal-link')\n\n // Avoid iOS opening Safari for \"new window\" navigation.\n anchor.removeAttribute('target')\n anchor.removeAttribute('rel')\n } catch {\n // If parsing fails, leave link as-is.\n }\n }\n}\n","/**\n * Type guard for Promise-like (thenable) values, used to normalize whatever\n * Staffbase's openLink returns.\n * @param {unknown} value - The value to test.\n * @returns {boolean} True when value has a callable then method.\n */\nexport const isPromiseLike = (value: unknown): value is PromiseLike<unknown> =>\n value !== null &&\n typeof value === 'object' &&\n 'then' in value &&\n typeof (value as PromiseLike<unknown>).then === 'function'\n","import type { TryOpenResult } from '../types/links/TryOpenResult'\nimport type { WindowWithStaffbase } from '../types/links/WindowWithStaffbase'\n\nimport { isPromiseLike } from './isPromiseLike'\n\n/**\n * Attempt to open a link using Staffbase's plugin util.openLink if available.\n * Ported from staffbase-alerts (typed Promise-like guard; smart-search used\n * `as any`).\n * @param {string} href - The target URL to open.\n * @returns {TryOpenResult} Whether Staffbase handled it and a promise if available.\n */\nexport const tryOpenWithStaffbase = (href: string): TryOpenResult => {\n try {\n if (typeof window === 'undefined') {\n return { handled: false }\n }\n const w = window as WindowWithStaffbase\n const open = w?.staffbase?.plugin?.util?.openLink\n const hasOpenLink = typeof open === 'function'\n if (!hasOpenLink) return { handled: false }\n // Pass empty options as the second parameter to align with openLink signature\n const maybePromise = open(href, {})\n // If a Promise is returned, normalize it\n let normalizedPromise: Promise<void> | undefined\n if (maybePromise && isPromiseLike(maybePromise)) {\n normalizedPromise = maybePromise as Promise<void>\n }\n return { handled: true, promise: normalizedPromise }\n } catch {\n return { handled: false }\n }\n}\n","import { isSafeNavigationHref } from './isSafeNavigationHref'\nimport { tryOpenWithStaffbase } from './tryOpenWithStaffbase'\n\n/**\n * Try to open with Staffbase; if not available, navigate normally in the same\n * tab. Ported from staffbase-alerts (the superset): it guards with\n * isSafeNavigationHref first, so scripted schemes never reach navigation.\n * @param {string} href - The target URL to open.\n * @returns {boolean} True if Staffbase handled the navigation, otherwise false.\n */\nexport const openStaffbaseAware = (href: string): boolean => {\n if (!href) return false\n\n // Never navigate to scripted/inline schemes (javascript:, data:, vbscript:),\n // which would execute in the session-bearing webview.\n if (!isSafeNavigationHref(href)) return false\n\n const result = tryOpenWithStaffbase(href)\n if (result.handled) {\n // Watchdog: if Staffbase openLink is a no-op, force navigation shortly after\n try {\n const beforeHref =\n typeof window !== 'undefined' ? window.location.href : undefined\n const timer = setTimeout(() => {\n try {\n const afterHref =\n typeof window !== 'undefined' ? window.location.href : undefined\n const visibility =\n typeof document !== 'undefined'\n ? document.visibilityState\n : undefined\n const noChange = beforeHref && afterHref && beforeHref === afterHref\n const stillVisible = visibility === 'visible'\n if (noChange && stillVisible && typeof window !== 'undefined') {\n window.location.assign(href)\n }\n } catch {\n // Ignore watchdog navigation failures.\n }\n }, 400)\n // If the Staffbase promise resolves, cancel the watchdog fallback\n try {\n result.promise\n ?.then(() => {\n clearTimeout(timer)\n })\n .catch(() => {\n // Keep the watchdog active on rejection.\n })\n } catch {\n // Ignore promise-wiring failures.\n }\n } catch {\n // Ignore watchdog setup failures.\n }\n return true\n }\n try {\n if (typeof window !== 'undefined') {\n window.location.assign(href)\n }\n } catch {\n // Ignore navigation failures.\n }\n return false\n}\n"],"mappings":";AAMA,IAAa,KAA4B,MACnC,EAAK,WAAW,YAAY,KAE5B,EAAK,WAAW,YAAY,IACvB,IAAI,EAAK,MAAM,EAAmB,MACpC,GCAI,KACX,GACA,MACkB;CAClB,IAAM,IAAU,GAAM,KAAK;CAI3B,IAHI,CAAC,KAGD,EAAQ,WAAW,GAAG,GAAG,OAAO;CAGpC,IAAM,IAAQ,EAAQ,YAAY;CAClC,IACE,EAAM,WAAW,SAAS,KAC1B,EAAM,WAAW,MAAM,KACvB,EAAM,WAAW,MAAM,KACvB,EAAM,WAAW,aAAa,KAC9B,EAAM,WAAW,OAAO,GAExB,OAAO;CAGT,IAAI;EACF,IAAM,IAAO,IAAI,IAAI,CAAO,GACtB,IAAM,IAAI,IAAI,GAAS,EAAK,SAAS,CAAC;EAG5C,IAAI,EAAI,WAAW,EAAK,QAAQ;GAC9B,IAAM,IAAc,EAAyB,EAAI,QAAQ;GAKzD,OAAO,IAJc,IACnB,GAAG,IAAc,EAAI,SAAS,EAAI,QAClC,EAAK,MAEA,EAAS,SAAS;EAC3B;EAGA,OAAO,EAAI,SAAS;CACtB,QAAQ;EACN,OAAO;CACT;AACF,GC/CM,IAA+B;CACnC;CACA;CACA;CACA;CACA;CACA;CACA;AACF,GASa,KAAsB,MAAyB;CAC1D,IAAM,IAAU,EAAI,KAAK;CACzB,IAAI,CAAC,GAAS,OAAO;CAErB,IAAI;EACF,IAAM,IACJ,OAAO,SAAW,MACd,OAAO,SAAS,SAChB,qBACA,IAAM,IAAI,IAAI,GAAS,CAAM;EAEnC,IAAI,EAAI,aAAa,YAAY,EAAI,aAAa,SAAS,OAAO;EAClE,IAAI,EAAI,WAAW,GAAQ,OAAO;EAElC,IAAM,IAAO,EAAI,SAAS,YAAY;EACtC,OAAO,EAA6B,MACjC,MAAW,MAAS,KAAU,EAAK,SAAS,IAAI,GAAQ,CAC3D;CACF,QAAQ;EACN,OAAO;CACT;AACF,GCnCa,KAAwB,MAA0B;CAC7D,IAAM,IAAU,EAAK,KAAK,EAAE,YAAY;CAGxC,OAFK,IAEE,EACL,EAAQ,WAAW,aAAa,KAChC,EAAQ,WAAW,OAAO,KAC1B,EAAQ,WAAW,WAAW,KALX;AAOvB,GCLa,KACX,GACA,MACS;CACT,IAAM,IAAS,GAAiB,KAAK,KAAK;CAC1C,IAAI,CAAC,GAAQ;CAEb,IAAM,IAAU,MAAM,KAAK,EAAK,iBAAiB,SAAS,CAAC;CAE3D,KAAK,IAAM,KAAU,GAAS;EAC5B,IAAM,IAAU,EAAO,aAAa,MAAM;EAC1C,IAAI,CAAC,GAAS;EAGd,IAAM,IAAQ,EAAQ,KAAK,EAAE,YAAY;EAEvC,QAAM,WAAW,GAAG,KACpB,EAAM,WAAW,SAAS,KAC1B,EAAM,WAAW,MAAM,KACvB,EAAM,WAAW,MAAM,KACvB,EAAM,WAAW,aAAa,KAC9B,EAAM,WAAW,OAAO,IAK1B,IAAI;GACF,IAAM,IAAM,IAAI,IAAI,GAAS,CAAM;GAGnC,IAAI,EAAI,WAAW,GAAQ;IAGzB,AAAI,EAAO,aAAa,QAAQ,MAAM,YACpC,EAAO,aAAa,OAAO,qBAAqB;IAElD;GACF;GAIA,IAAM,IAAyB,GADX,EAAI,WAAW,EAAI,SAAS,EAAI,OAEjD,QAAQ,iBAAiB,GAAG,EAC5B,QAAQ,iBAAiB,GAAG;GAS/B,AARA,EAAO,aAAa,QAAQ,CAAsB,GAIlD,EAAO,UAAU,IAAI,eAAe,GAGpC,EAAO,gBAAgB,QAAQ,GAC/B,EAAO,gBAAgB,KAAK;EAC9B,QAAQ,CAER;CACF;AACF,GC/Da,KAAiB,MAE5B,OAAO,KAAU,cADjB,KAEA,UAAU,KACV,OAAQ,EAA+B,QAAS,YCErC,KAAwB,MAAgC;CACnE,IAAI;EACF,IAAI,OAAO,SAAW,KACpB,OAAO,EAAE,SAAS,GAAM;EAG1B,IAAM,IAAO,QAAG,WAAW,QAAQ,MAAM;EAEzC,IADoB,OAAO,KAAS,YAClB,OAAO,EAAE,SAAS,GAAM;EAE1C,IAAM,IAAe,EAAK,GAAM,CAAC,CAAC,GAE9B;EAIJ,OAHI,KAAgB,EAAc,CAAY,MAC5C,IAAoB,IAEf;GAAE,SAAS;GAAM,SAAS;EAAkB;CACrD,QAAQ;EACN,OAAO,EAAE,SAAS,GAAM;CAC1B;AACF,GCtBa,KAAsB,MAA0B;CAK3D,IAJI,CAAC,KAID,CAAC,EAAqB,CAAI,GAAG,OAAO;CAExC,IAAM,IAAS,EAAqB,CAAI;CACxC,IAAI,EAAO,SAAS;EAElB,IAAI;GACF,IAAM,IACJ,OAAO,SAAW,MAAc,OAAO,SAAS,OAAO,KAAA,GACnD,IAAQ,iBAAiB;IAC7B,IAAI;KACF,IAAM,IACJ,OAAO,SAAW,MAAc,OAAO,SAAS,OAAO,KAAA,GACnD,IACJ,OAAO,WAAa,MAChB,SAAS,kBACT,KAAA;KAGN,AAFiB,KAAc,KAAa,MAAe,KACtC,MAAe,aACJ,OAAO,SAAW,OAChD,OAAO,SAAS,OAAO,CAAI;IAE/B,QAAQ,CAER;GACF,GAAG,GAAG;GAEN,IAAI;IACF,EAAO,SACH,WAAW;KACX,aAAa,CAAK;IACpB,CAAC,EACA,YAAY,CAEb,CAAC;GACL,QAAQ,CAER;EACF,QAAQ,CAER;EACA,OAAO;CACT;CACA,IAAI;EACF,AAAI,OAAO,SAAW,OACpB,OAAO,SAAS,OAAO,CAAI;CAE/B,QAAQ,CAER;CACA,OAAO;AACT"}
@@ -0,0 +1,81 @@
1
+ //#region src/widgets/defaultWidgetOnError.ts
2
+ var e = (e, t) => {
3
+ t === "manager-unavailable" && console.warn("[staffbase-utils] Staffbase widget manager unavailable; embedded widgets were not rendered.", e);
4
+ }, t = () => {
5
+ if (typeof window > "u") return null;
6
+ let e = window.staffbase?.content?.widgetMgr;
7
+ return typeof e == "function" ? e : null;
8
+ }, n = () => typeof window > "u" ? null : window.staffbase?.content?.widgetMgr?.prototype ?? null, r = (e) => typeof e._extractWidgets == "function" && typeof e._renderWidget == "function", i = (e) => e instanceof TypeError && (e.message?.includes("each") || e.message?.includes("undefined is not an object")), a = Promise.resolve(), o = /* @__PURE__ */ new WeakMap(), s = !1, c = async (e, a, c, l, u, d) => {
9
+ let f = 0, p = !1;
10
+ for (; f < l;) {
11
+ if (f++, o.get(a) !== c) return {
12
+ ok: !1,
13
+ reason: "cancelled"
14
+ };
15
+ if (typeof e.querySelectorAll != "function") return {
16
+ ok: !1,
17
+ reason: "no-container"
18
+ };
19
+ let s = t();
20
+ if (s) {
21
+ p = !0;
22
+ try {
23
+ let t = new s(void 0, !1);
24
+ if (typeof t.render == "function") return await t.render(e), {
25
+ ok: !0,
26
+ rendered: 0
27
+ };
28
+ } catch {}
29
+ }
30
+ let m = n();
31
+ if (m && r(m)) {
32
+ p = !0, Array.isArray(m._widgets) || (m._widgets = []);
33
+ let t;
34
+ try {
35
+ t = m._extractWidgets(e);
36
+ } catch (e) {
37
+ d(e, "extract"), t = [];
38
+ }
39
+ if (t.length > 0) {
40
+ let n = 0;
41
+ for (let r of t) {
42
+ if (o.get(a) !== c) return {
43
+ ok: !1,
44
+ reason: "cancelled"
45
+ };
46
+ try {
47
+ m._renderWidget.call(m, e, r), n++;
48
+ } catch (e) {
49
+ i(e) || d(e, "render-widget");
50
+ }
51
+ }
52
+ return {
53
+ ok: !0,
54
+ rendered: n
55
+ };
56
+ }
57
+ }
58
+ if (f >= l) break;
59
+ await new Promise((e) => setTimeout(e, u));
60
+ }
61
+ return p ? {
62
+ ok: !1,
63
+ reason: "no-widgets"
64
+ } : (s || (s = !0, d(/* @__PURE__ */ Error("Staffbase widget manager unavailable after retries."), "manager-unavailable")), {
65
+ ok: !1,
66
+ reason: "manager-unavailable"
67
+ });
68
+ }, l = (t, n = {}) => {
69
+ if (!t) return Promise.resolve({
70
+ ok: !1,
71
+ reason: "no-container"
72
+ });
73
+ let { maxRetries: r = 10, retryDelay: i = 300, onError: s = e, cancelKey: l = t } = n, u = (o.get(l) ?? 0) + 1;
74
+ o.set(l, u);
75
+ let d = a.then(() => c(t, l, u, r, i, s));
76
+ return a = d.then(() => void 0, () => void 0), d;
77
+ };
78
+ //#endregion
79
+ export { l as t };
80
+
81
+ //# sourceMappingURL=renderWidgets-CeIczubt.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"renderWidgets-CeIczubt.mjs","names":[],"sources":["../src/widgets/defaultWidgetOnError.ts","../src/widgets/getWidgetManagerConstructor.ts","../src/widgets/getWidgetManagerPrototype.ts","../src/widgets/hasRequiredWidgetManagerMethods.ts","../src/widgets/isKnownStaffbaseRenderError.ts","../src/widgets/renderWidgets.ts"],"sourcesContent":["/**\n * Default onError for renderWidgets: warns only on the critical\n * 'manager-unavailable' context so a platform regression stays visible in logs\n * without spamming per-widget noise. Consumers can pass their own onError.\n * @param {unknown} error - The swallowed error.\n * @param {string} context - Where it happened.\n * @returns {void}\n */\nexport const defaultWidgetOnError = (error: unknown, context: string): void => {\n if (context === 'manager-unavailable') {\n console.warn(\n '[staffbase-utils] Staffbase widget manager unavailable; embedded widgets were not rendered.',\n error,\n )\n }\n}\n","import type { StaffbaseWidgetManagerConstructor } from '../types/widgets/StaffbaseWidgetManagerConstructor'\n\n/**\n * Resolves the host widget-manager constructor from the global Staffbase object,\n * or null when it is not a function (older runtimes / platform regression).\n * @returns {StaffbaseWidgetManagerConstructor | null} The constructor or null.\n */\nexport const getWidgetManagerConstructor =\n (): StaffbaseWidgetManagerConstructor | null => {\n if (typeof window === 'undefined') return null\n const ctor = (\n window as unknown as {\n staffbase?: { content?: { widgetMgr?: unknown } }\n }\n ).staffbase?.content?.widgetMgr\n return typeof ctor === 'function'\n ? (ctor as StaffbaseWidgetManagerConstructor)\n : null\n }\n","import type { StaffbaseWidgetManagerPrototype } from '../types/widgets/StaffbaseWidgetManagerPrototype'\n\n/**\n * Resolves the host widget-manager prototype (the private fallback API) from the\n * global Staffbase object, or null when it is unavailable.\n * @returns {StaffbaseWidgetManagerPrototype | null} The prototype or null.\n */\nexport const getWidgetManagerPrototype =\n (): StaffbaseWidgetManagerPrototype | null => {\n if (typeof window === 'undefined') return null\n const proto = (\n window as unknown as {\n staffbase?: {\n content?: {\n widgetMgr?: { prototype?: StaffbaseWidgetManagerPrototype }\n }\n }\n }\n ).staffbase?.content?.widgetMgr?.prototype\n return proto ?? null\n }\n","import type { StaffbaseWidgetManagerPrototype } from '../types/widgets/StaffbaseWidgetManagerPrototype'\n\n/**\n * Type guard asserting the widget-manager prototype exposes the methods the\n * prototype render path needs.\n * @param {StaffbaseWidgetManagerPrototype} widgetMgr - The resolved prototype.\n * @returns {boolean} True when both _extractWidgets and _renderWidget are functions.\n */\nexport const hasRequiredWidgetManagerMethods = (\n widgetMgr: StaffbaseWidgetManagerPrototype,\n): widgetMgr is StaffbaseWidgetManagerPrototype & {\n _extractWidgets: (container: HTMLElement) => unknown[]\n _renderWidget: (\n this: StaffbaseWidgetManagerPrototype,\n container: HTMLElement,\n widget: unknown,\n ) => void\n} =>\n typeof widgetMgr._extractWidgets === 'function' &&\n typeof widgetMgr._renderWidget === 'function'\n","/**\n * Detects known, benign internal Staffbase errors thrown by `_renderWidget` for\n * individual widgets, so they can be swallowed without aborting the batch.\n * Promoted from unacknowledged-bulletins (`each` / `undefined is not an object`).\n * @param {unknown} error - The thrown value.\n * @returns {boolean} True when the error is a known internal render error.\n */\nexport const isKnownStaffbaseRenderError = (error: unknown): boolean =>\n error instanceof TypeError &&\n (error.message?.includes('each') ||\n error.message?.includes('undefined is not an object'))\n","import type { RenderWidgetsOptions } from '../types/widgets/RenderWidgetsOptions'\nimport type { RenderWidgetsResult } from '../types/widgets/RenderWidgetsResult'\n\nimport { defaultWidgetOnError } from './defaultWidgetOnError'\nimport { getWidgetManagerConstructor } from './getWidgetManagerConstructor'\nimport { getWidgetManagerPrototype } from './getWidgetManagerPrototype'\nimport { hasRequiredWidgetManagerMethods } from './hasRequiredWidgetManagerMethods'\nimport { isKnownStaffbaseRenderError } from './isKnownStaffbaseRenderError'\n\n// Global serialization chain: all renders run one at a time so they never race\n// on the host's shared `_widgets` array (unacknowledged-bulletins' lock, made\n// queue-based). Per-cancelKey run ids drop superseded renders (alerts' WeakMap\n// cancellation; no AbortController, for old webviews). warn-once keeps a missing\n// manager observable without spam (alerts).\nlet queue: Promise<unknown> = Promise.resolve()\nconst cancelTokens = new WeakMap<object, number>()\nlet hasWarnedManagerUnavailable = false\n\n/**\n * Runs the retry loop for a single render: constructor path first, prototype\n * fallback, with cancellation checks and known-error swallowing.\n * @param {HTMLElement} container - The container to render widgets into.\n * @param {object} cancelKey - The cancellation key for this render.\n * @param {number} runId - This render's run id for the cancel key.\n * @param {number} maxRetries - Maximum render attempts.\n * @param {number} retryDelay - Delay between attempts, in milliseconds.\n * @param {(error: unknown, context: string) => void} onError - Error reporter.\n * @returns {Promise<RenderWidgetsResult>} The typed render result.\n */\nconst executeRender = async (\n container: HTMLElement,\n cancelKey: object,\n runId: number,\n maxRetries: number,\n retryDelay: number,\n onError: (error: unknown, context: string) => void,\n): Promise<RenderWidgetsResult> => {\n let attempts = 0\n let managerSeen = false\n\n while (attempts < maxRetries) {\n attempts++\n if (cancelTokens.get(cancelKey) !== runId)\n return { ok: false, reason: 'cancelled' }\n if (typeof container.querySelectorAll !== 'function') {\n return { ok: false, reason: 'no-container' }\n }\n\n // Prefer Staffbase's real widget manager (closest to host behavior).\n const ctor = getWidgetManagerConstructor()\n if (ctor) {\n managerSeen = true\n try {\n const instance = new ctor(undefined, false)\n if (typeof instance.render === 'function') {\n await instance.render(container)\n return { ok: true, rendered: 0 }\n }\n } catch {\n // Fall back to the private prototype path below.\n }\n }\n\n const proto = getWidgetManagerPrototype()\n if (proto && hasRequiredWidgetManagerMethods(proto)) {\n managerSeen = true\n if (!Array.isArray(proto._widgets)) proto._widgets = []\n\n let widgets: unknown[]\n try {\n widgets = proto._extractWidgets(container)\n } catch (error) {\n onError(error, 'extract')\n widgets = []\n }\n\n if (widgets.length > 0) {\n let rendered = 0\n for (const widget of widgets) {\n if (cancelTokens.get(cancelKey) !== runId) {\n return { ok: false, reason: 'cancelled' }\n }\n try {\n proto._renderWidget.call(proto, container, widget)\n rendered++\n } catch (error) {\n if (!isKnownStaffbaseRenderError(error))\n onError(error, 'render-widget')\n }\n }\n return { ok: true, rendered }\n }\n }\n\n if (attempts >= maxRetries) break\n await new Promise((resolve) => setTimeout(resolve, retryDelay))\n }\n\n if (!managerSeen) {\n if (!hasWarnedManagerUnavailable) {\n hasWarnedManagerUnavailable = true\n onError(\n new Error('Staffbase widget manager unavailable after retries.'),\n 'manager-unavailable',\n )\n }\n return { ok: false, reason: 'manager-unavailable' }\n }\n return { ok: false, reason: 'no-widgets' }\n}\n\n/**\n * Renders the widgets embedded in `container` using the host's private widget\n * manager. Superset of the four widget services: constructor path first with a\n * prototype fallback, a global queue that serializes renders, per-cancelKey\n * cancellation of superseded renders, configurable retries, swallowing of known\n * internal Staffbase errors, and a once-per-session warning when the manager is\n * missing. Returns a typed result so callers can react (e.g. mark a\n * data-widget-render-error attribute).\n * @param {HTMLElement | null} container - The element whose embedded widgets are rendered.\n * @param {RenderWidgetsOptions} options - Retry, error and cancellation options.\n * @returns {Promise<RenderWidgetsResult>} The typed render result.\n */\nexport const renderWidgets = (\n container: HTMLElement | null,\n options: RenderWidgetsOptions = {},\n): Promise<RenderWidgetsResult> => {\n if (!container) return Promise.resolve({ ok: false, reason: 'no-container' })\n\n const {\n maxRetries = 10,\n retryDelay = 300,\n onError = defaultWidgetOnError,\n cancelKey = container,\n } = options\n\n // Bump the run id synchronously so any older in-flight/queued render for the\n // same key sees itself as superseded.\n const runId = (cancelTokens.get(cancelKey) ?? 0) + 1\n cancelTokens.set(cancelKey, runId)\n\n const run = queue.then(() =>\n executeRender(container, cancelKey, runId, maxRetries, retryDelay, onError),\n )\n // Keep the queue chain alive regardless of individual outcomes.\n queue = run.then(\n () => undefined,\n () => undefined,\n )\n return run\n}\n"],"mappings":";AAQA,IAAa,KAAwB,GAAgB,MAA0B;CAC7E,AAAI,MAAY,yBACd,QAAQ,KACN,+FACA,CACF;AAEJ,GCRa,UACqC;CAC9C,IAAI,OAAO,SAAW,KAAa,OAAO;CAC1C,IAAM,IACJ,OAGA,WAAW,SAAS;CACtB,OAAO,OAAO,KAAS,aAClB,IACD;AACN,GCXW,UAEL,OAAO,SAAW,MAAoB,OAExC,OAOA,WAAW,SAAS,WAAW,aACjB,MCXP,KACX,MASA,OAAO,EAAU,mBAAoB,cACrC,OAAO,EAAU,iBAAkB,YCZxB,KAA+B,MAC1C,aAAiB,cAChB,EAAM,SAAS,SAAS,MAAM,KAC7B,EAAM,SAAS,SAAS,4BAA4B,ICIpD,IAA0B,QAAQ,QAAQ,GACxC,oBAAe,IAAI,QAAwB,GAC7C,IAA8B,IAa5B,IAAgB,OACpB,GACA,GACA,GACA,GACA,GACA,MACiC;CACjC,IAAI,IAAW,GACX,IAAc;CAElB,OAAO,IAAW,IAAY;EAE5B,IADA,KACI,EAAa,IAAI,CAAS,MAAM,GAClC,OAAO;GAAE,IAAI;GAAO,QAAQ;EAAY;EAC1C,IAAI,OAAO,EAAU,oBAAqB,YACxC,OAAO;GAAE,IAAI;GAAO,QAAQ;EAAe;EAI7C,IAAM,IAAO,EAA4B;EACzC,IAAI,GAAM;GACR,IAAc;GACd,IAAI;IACF,IAAM,IAAW,IAAI,EAAK,KAAA,GAAW,EAAK;IAC1C,IAAI,OAAO,EAAS,UAAW,YAE7B,OADA,MAAM,EAAS,OAAO,CAAS,GACxB;KAAE,IAAI;KAAM,UAAU;IAAE;GAEnC,QAAQ,CAER;EACF;EAEA,IAAM,IAAQ,EAA0B;EACxC,IAAI,KAAS,EAAgC,CAAK,GAAG;GAEnD,AADA,IAAc,IACT,MAAM,QAAQ,EAAM,QAAQ,MAAG,EAAM,WAAW,CAAC;GAEtD,IAAI;GACJ,IAAI;IACF,IAAU,EAAM,gBAAgB,CAAS;GAC3C,SAAS,GAAO;IAEd,AADA,EAAQ,GAAO,SAAS,GACxB,IAAU,CAAC;GACb;GAEA,IAAI,EAAQ,SAAS,GAAG;IACtB,IAAI,IAAW;IACf,KAAK,IAAM,KAAU,GAAS;KAC5B,IAAI,EAAa,IAAI,CAAS,MAAM,GAClC,OAAO;MAAE,IAAI;MAAO,QAAQ;KAAY;KAE1C,IAAI;MAEF,AADA,EAAM,cAAc,KAAK,GAAO,GAAW,CAAM,GACjD;KACF,SAAS,GAAO;MACd,AAAK,EAA4B,CAAK,KACpC,EAAQ,GAAO,eAAe;KAClC;IACF;IACA,OAAO;KAAE,IAAI;KAAM;IAAS;GAC9B;EACF;EAEA,IAAI,KAAY,GAAY;EAC5B,MAAM,IAAI,SAAS,MAAY,WAAW,GAAS,CAAU,CAAC;CAChE;CAYA,OAVK,IAUE;EAAE,IAAI;EAAO,QAAQ;CAAa,KATlC,MACH,IAA8B,IAC9B,EACE,gBAAI,MAAM,qDAAqD,GAC/D,qBACF,IAEK;EAAE,IAAI;EAAO,QAAQ;CAAsB;AAGtD,GAca,KACX,GACA,IAAgC,CAAC,MACA;CACjC,IAAI,CAAC,GAAW,OAAO,QAAQ,QAAQ;EAAE,IAAI;EAAO,QAAQ;CAAe,CAAC;CAE5E,IAAM,EACJ,gBAAa,IACb,gBAAa,KACb,aAAU,GACV,eAAY,MACV,GAIE,KAAS,EAAa,IAAI,CAAS,KAAK,KAAK;CACnD,EAAa,IAAI,GAAW,CAAK;CAEjC,IAAM,IAAM,EAAM,WAChB,EAAc,GAAW,GAAW,GAAO,GAAY,GAAY,CAAO,CAC5E;CAMA,OAJA,IAAQ,EAAI,WACJ,KAAA,SACA,KAAA,CACR,GACO;AACT"}
@@ -0,0 +1,2 @@
1
+ var e=(e,t)=>{t===`manager-unavailable`&&console.warn(`[staffbase-utils] Staffbase widget manager unavailable; embedded widgets were not rendered.`,e)},t=()=>{if(typeof window>`u`)return null;let e=window.staffbase?.content?.widgetMgr;return typeof e==`function`?e:null},n=()=>typeof window>`u`?null:window.staffbase?.content?.widgetMgr?.prototype??null,r=e=>typeof e._extractWidgets==`function`&&typeof e._renderWidget==`function`,i=e=>e instanceof TypeError&&(e.message?.includes(`each`)||e.message?.includes(`undefined is not an object`)),a=Promise.resolve(),o=new WeakMap,s=!1,c=async(e,a,c,l,u,d)=>{let f=0,p=!1;for(;f<l;){if(f++,o.get(a)!==c)return{ok:!1,reason:`cancelled`};if(typeof e.querySelectorAll!=`function`)return{ok:!1,reason:`no-container`};let s=t();if(s){p=!0;try{let t=new s(void 0,!1);if(typeof t.render==`function`)return await t.render(e),{ok:!0,rendered:0}}catch{}}let m=n();if(m&&r(m)){p=!0,Array.isArray(m._widgets)||(m._widgets=[]);let t;try{t=m._extractWidgets(e)}catch(e){d(e,`extract`),t=[]}if(t.length>0){let n=0;for(let r of t){if(o.get(a)!==c)return{ok:!1,reason:`cancelled`};try{m._renderWidget.call(m,e,r),n++}catch(e){i(e)||d(e,`render-widget`)}}return{ok:!0,rendered:n}}}if(f>=l)break;await new Promise(e=>setTimeout(e,u))}return p?{ok:!1,reason:`no-widgets`}:(s||(s=!0,d(Error(`Staffbase widget manager unavailable after retries.`),`manager-unavailable`)),{ok:!1,reason:`manager-unavailable`})},l=(t,n={})=>{if(!t)return Promise.resolve({ok:!1,reason:`no-container`});let{maxRetries:r=10,retryDelay:i=300,onError:s=e,cancelKey:l=t}=n,u=(o.get(l)??0)+1;o.set(l,u);let d=a.then(()=>c(t,l,u,r,i,s));return a=d.then(()=>void 0,()=>void 0),d};Object.defineProperty(exports,"t",{enumerable:!0,get:function(){return l}});
2
+ //# sourceMappingURL=renderWidgets-EbJq9Wn6.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"renderWidgets-EbJq9Wn6.js","names":[],"sources":["../src/widgets/defaultWidgetOnError.ts","../src/widgets/getWidgetManagerConstructor.ts","../src/widgets/getWidgetManagerPrototype.ts","../src/widgets/hasRequiredWidgetManagerMethods.ts","../src/widgets/isKnownStaffbaseRenderError.ts","../src/widgets/renderWidgets.ts"],"sourcesContent":["/**\n * Default onError for renderWidgets: warns only on the critical\n * 'manager-unavailable' context so a platform regression stays visible in logs\n * without spamming per-widget noise. Consumers can pass their own onError.\n * @param {unknown} error - The swallowed error.\n * @param {string} context - Where it happened.\n * @returns {void}\n */\nexport const defaultWidgetOnError = (error: unknown, context: string): void => {\n if (context === 'manager-unavailable') {\n console.warn(\n '[staffbase-utils] Staffbase widget manager unavailable; embedded widgets were not rendered.',\n error,\n )\n }\n}\n","import type { StaffbaseWidgetManagerConstructor } from '../types/widgets/StaffbaseWidgetManagerConstructor'\n\n/**\n * Resolves the host widget-manager constructor from the global Staffbase object,\n * or null when it is not a function (older runtimes / platform regression).\n * @returns {StaffbaseWidgetManagerConstructor | null} The constructor or null.\n */\nexport const getWidgetManagerConstructor =\n (): StaffbaseWidgetManagerConstructor | null => {\n if (typeof window === 'undefined') return null\n const ctor = (\n window as unknown as {\n staffbase?: { content?: { widgetMgr?: unknown } }\n }\n ).staffbase?.content?.widgetMgr\n return typeof ctor === 'function'\n ? (ctor as StaffbaseWidgetManagerConstructor)\n : null\n }\n","import type { StaffbaseWidgetManagerPrototype } from '../types/widgets/StaffbaseWidgetManagerPrototype'\n\n/**\n * Resolves the host widget-manager prototype (the private fallback API) from the\n * global Staffbase object, or null when it is unavailable.\n * @returns {StaffbaseWidgetManagerPrototype | null} The prototype or null.\n */\nexport const getWidgetManagerPrototype =\n (): StaffbaseWidgetManagerPrototype | null => {\n if (typeof window === 'undefined') return null\n const proto = (\n window as unknown as {\n staffbase?: {\n content?: {\n widgetMgr?: { prototype?: StaffbaseWidgetManagerPrototype }\n }\n }\n }\n ).staffbase?.content?.widgetMgr?.prototype\n return proto ?? null\n }\n","import type { StaffbaseWidgetManagerPrototype } from '../types/widgets/StaffbaseWidgetManagerPrototype'\n\n/**\n * Type guard asserting the widget-manager prototype exposes the methods the\n * prototype render path needs.\n * @param {StaffbaseWidgetManagerPrototype} widgetMgr - The resolved prototype.\n * @returns {boolean} True when both _extractWidgets and _renderWidget are functions.\n */\nexport const hasRequiredWidgetManagerMethods = (\n widgetMgr: StaffbaseWidgetManagerPrototype,\n): widgetMgr is StaffbaseWidgetManagerPrototype & {\n _extractWidgets: (container: HTMLElement) => unknown[]\n _renderWidget: (\n this: StaffbaseWidgetManagerPrototype,\n container: HTMLElement,\n widget: unknown,\n ) => void\n} =>\n typeof widgetMgr._extractWidgets === 'function' &&\n typeof widgetMgr._renderWidget === 'function'\n","/**\n * Detects known, benign internal Staffbase errors thrown by `_renderWidget` for\n * individual widgets, so they can be swallowed without aborting the batch.\n * Promoted from unacknowledged-bulletins (`each` / `undefined is not an object`).\n * @param {unknown} error - The thrown value.\n * @returns {boolean} True when the error is a known internal render error.\n */\nexport const isKnownStaffbaseRenderError = (error: unknown): boolean =>\n error instanceof TypeError &&\n (error.message?.includes('each') ||\n error.message?.includes('undefined is not an object'))\n","import type { RenderWidgetsOptions } from '../types/widgets/RenderWidgetsOptions'\nimport type { RenderWidgetsResult } from '../types/widgets/RenderWidgetsResult'\n\nimport { defaultWidgetOnError } from './defaultWidgetOnError'\nimport { getWidgetManagerConstructor } from './getWidgetManagerConstructor'\nimport { getWidgetManagerPrototype } from './getWidgetManagerPrototype'\nimport { hasRequiredWidgetManagerMethods } from './hasRequiredWidgetManagerMethods'\nimport { isKnownStaffbaseRenderError } from './isKnownStaffbaseRenderError'\n\n// Global serialization chain: all renders run one at a time so they never race\n// on the host's shared `_widgets` array (unacknowledged-bulletins' lock, made\n// queue-based). Per-cancelKey run ids drop superseded renders (alerts' WeakMap\n// cancellation; no AbortController, for old webviews). warn-once keeps a missing\n// manager observable without spam (alerts).\nlet queue: Promise<unknown> = Promise.resolve()\nconst cancelTokens = new WeakMap<object, number>()\nlet hasWarnedManagerUnavailable = false\n\n/**\n * Runs the retry loop for a single render: constructor path first, prototype\n * fallback, with cancellation checks and known-error swallowing.\n * @param {HTMLElement} container - The container to render widgets into.\n * @param {object} cancelKey - The cancellation key for this render.\n * @param {number} runId - This render's run id for the cancel key.\n * @param {number} maxRetries - Maximum render attempts.\n * @param {number} retryDelay - Delay between attempts, in milliseconds.\n * @param {(error: unknown, context: string) => void} onError - Error reporter.\n * @returns {Promise<RenderWidgetsResult>} The typed render result.\n */\nconst executeRender = async (\n container: HTMLElement,\n cancelKey: object,\n runId: number,\n maxRetries: number,\n retryDelay: number,\n onError: (error: unknown, context: string) => void,\n): Promise<RenderWidgetsResult> => {\n let attempts = 0\n let managerSeen = false\n\n while (attempts < maxRetries) {\n attempts++\n if (cancelTokens.get(cancelKey) !== runId)\n return { ok: false, reason: 'cancelled' }\n if (typeof container.querySelectorAll !== 'function') {\n return { ok: false, reason: 'no-container' }\n }\n\n // Prefer Staffbase's real widget manager (closest to host behavior).\n const ctor = getWidgetManagerConstructor()\n if (ctor) {\n managerSeen = true\n try {\n const instance = new ctor(undefined, false)\n if (typeof instance.render === 'function') {\n await instance.render(container)\n return { ok: true, rendered: 0 }\n }\n } catch {\n // Fall back to the private prototype path below.\n }\n }\n\n const proto = getWidgetManagerPrototype()\n if (proto && hasRequiredWidgetManagerMethods(proto)) {\n managerSeen = true\n if (!Array.isArray(proto._widgets)) proto._widgets = []\n\n let widgets: unknown[]\n try {\n widgets = proto._extractWidgets(container)\n } catch (error) {\n onError(error, 'extract')\n widgets = []\n }\n\n if (widgets.length > 0) {\n let rendered = 0\n for (const widget of widgets) {\n if (cancelTokens.get(cancelKey) !== runId) {\n return { ok: false, reason: 'cancelled' }\n }\n try {\n proto._renderWidget.call(proto, container, widget)\n rendered++\n } catch (error) {\n if (!isKnownStaffbaseRenderError(error))\n onError(error, 'render-widget')\n }\n }\n return { ok: true, rendered }\n }\n }\n\n if (attempts >= maxRetries) break\n await new Promise((resolve) => setTimeout(resolve, retryDelay))\n }\n\n if (!managerSeen) {\n if (!hasWarnedManagerUnavailable) {\n hasWarnedManagerUnavailable = true\n onError(\n new Error('Staffbase widget manager unavailable after retries.'),\n 'manager-unavailable',\n )\n }\n return { ok: false, reason: 'manager-unavailable' }\n }\n return { ok: false, reason: 'no-widgets' }\n}\n\n/**\n * Renders the widgets embedded in `container` using the host's private widget\n * manager. Superset of the four widget services: constructor path first with a\n * prototype fallback, a global queue that serializes renders, per-cancelKey\n * cancellation of superseded renders, configurable retries, swallowing of known\n * internal Staffbase errors, and a once-per-session warning when the manager is\n * missing. Returns a typed result so callers can react (e.g. mark a\n * data-widget-render-error attribute).\n * @param {HTMLElement | null} container - The element whose embedded widgets are rendered.\n * @param {RenderWidgetsOptions} options - Retry, error and cancellation options.\n * @returns {Promise<RenderWidgetsResult>} The typed render result.\n */\nexport const renderWidgets = (\n container: HTMLElement | null,\n options: RenderWidgetsOptions = {},\n): Promise<RenderWidgetsResult> => {\n if (!container) return Promise.resolve({ ok: false, reason: 'no-container' })\n\n const {\n maxRetries = 10,\n retryDelay = 300,\n onError = defaultWidgetOnError,\n cancelKey = container,\n } = options\n\n // Bump the run id synchronously so any older in-flight/queued render for the\n // same key sees itself as superseded.\n const runId = (cancelTokens.get(cancelKey) ?? 0) + 1\n cancelTokens.set(cancelKey, runId)\n\n const run = queue.then(() =>\n executeRender(container, cancelKey, runId, maxRetries, retryDelay, onError),\n )\n // Keep the queue chain alive regardless of individual outcomes.\n queue = run.then(\n () => undefined,\n () => undefined,\n )\n return run\n}\n"],"mappings":"AAQA,IAAa,GAAwB,EAAgB,IAA0B,CACzE,IAAY,uBACd,QAAQ,KACN,8FACA,CACF,CAEJ,ECRa,MACqC,CAC9C,GAAI,OAAO,OAAW,IAAa,OAAO,KAC1C,IAAM,EACJ,OAGA,WAAW,SAAS,UACtB,OAAO,OAAO,GAAS,WAClB,EACD,IACN,ECXW,MAEL,OAAO,OAAW,IAAoB,KAExC,OAOA,WAAW,SAAS,WAAW,WACjB,KCXP,EACX,GASA,OAAO,EAAU,iBAAoB,YACrC,OAAO,EAAU,eAAkB,WCZxB,EAA+B,GAC1C,aAAiB,YAChB,EAAM,SAAS,SAAS,MAAM,GAC7B,EAAM,SAAS,SAAS,4BAA4B,GCIpD,EAA0B,QAAQ,QAAQ,EACxC,EAAe,IAAI,QACrB,EAA8B,GAa5B,EAAgB,MACpB,EACA,EACA,EACA,EACA,EACA,IACiC,CACjC,IAAI,EAAW,EACX,EAAc,GAElB,KAAO,EAAW,GAAY,CAE5B,GADA,IACI,EAAa,IAAI,CAAS,IAAM,EAClC,MAAO,CAAE,GAAI,GAAO,OAAQ,WAAY,EAC1C,GAAI,OAAO,EAAU,kBAAqB,WACxC,MAAO,CAAE,GAAI,GAAO,OAAQ,cAAe,EAI7C,IAAM,EAAO,EAA4B,EACzC,GAAI,EAAM,CACR,EAAc,GACd,GAAI,CACF,IAAM,EAAW,IAAI,EAAK,IAAA,GAAW,EAAK,EAC1C,GAAI,OAAO,EAAS,QAAW,WAE7B,OADA,MAAM,EAAS,OAAO,CAAS,EACxB,CAAE,GAAI,GAAM,SAAU,CAAE,CAEnC,MAAQ,CAER,CACF,CAEA,IAAM,EAAQ,EAA0B,EACxC,GAAI,GAAS,EAAgC,CAAK,EAAG,CACnD,EAAc,GACT,MAAM,QAAQ,EAAM,QAAQ,IAAG,EAAM,SAAW,CAAC,GAEtD,IAAI,EACJ,GAAI,CACF,EAAU,EAAM,gBAAgB,CAAS,CAC3C,OAAS,EAAO,CACd,EAAQ,EAAO,SAAS,EACxB,EAAU,CAAC,CACb,CAEA,GAAI,EAAQ,OAAS,EAAG,CACtB,IAAI,EAAW,EACf,IAAK,IAAM,KAAU,EAAS,CAC5B,GAAI,EAAa,IAAI,CAAS,IAAM,EAClC,MAAO,CAAE,GAAI,GAAO,OAAQ,WAAY,EAE1C,GAAI,CACF,EAAM,cAAc,KAAK,EAAO,EAAW,CAAM,EACjD,GACF,OAAS,EAAO,CACT,EAA4B,CAAK,GACpC,EAAQ,EAAO,eAAe,CAClC,CACF,CACA,MAAO,CAAE,GAAI,GAAM,UAAS,CAC9B,CACF,CAEA,GAAI,GAAY,EAAY,MAC5B,MAAM,IAAI,QAAS,GAAY,WAAW,EAAS,CAAU,CAAC,CAChE,CAYA,OAVK,EAUE,CAAE,GAAI,GAAO,OAAQ,YAAa,GATlC,IACH,EAA8B,GAC9B,EACM,MAAM,qDAAqD,EAC/D,qBACF,GAEK,CAAE,GAAI,GAAO,OAAQ,qBAAsB,EAGtD,EAca,GACX,EACA,EAAgC,CAAC,IACA,CACjC,GAAI,CAAC,EAAW,OAAO,QAAQ,QAAQ,CAAE,GAAI,GAAO,OAAQ,cAAe,CAAC,EAE5E,GAAM,CACJ,aAAa,GACb,aAAa,IACb,UAAU,EACV,YAAY,GACV,EAIE,GAAS,EAAa,IAAI,CAAS,GAAK,GAAK,EACnD,EAAa,IAAI,EAAW,CAAK,EAEjC,IAAM,EAAM,EAAM,SAChB,EAAc,EAAW,EAAW,EAAO,EAAY,EAAY,CAAO,CAC5E,EAMA,MAJA,GAAQ,EAAI,SACJ,IAAA,OACA,IAAA,EACR,EACO,CACT"}
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Returns an absolute URL string that is safe to pass into Staffbase's
3
+ * `openLink()`. We intentionally do NOT require `/openlink/`; if the platform
4
+ * strips it, links can still open in-app by delegating to `openLink()`. Ported
5
+ * from staffbase-alerts (identical to unack).
6
+ * @param {string | null | undefined} href - Raw href as found on an <a> element.
7
+ * @param {string} baseUrl - Base URL used to resolve relative hrefs (usually window.location.origin).
8
+ * @returns {string | null} An absolute URL string, or null if it should not be handled.
9
+ */
10
+ export declare const getInAppOpenLinkTarget: (href: string | null | undefined, baseUrl: string) => string | null;
11
+ //# sourceMappingURL=getInAppOpenLinkTarget.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"getInAppOpenLinkTarget.d.ts","sourceRoot":"","sources":["../../../src/links/getInAppOpenLinkTarget.ts"],"names":[],"mappings":"AAEA;;;;;;;;GAQG;AACH,eAAO,MAAM,sBAAsB,GACjC,MAAM,MAAM,GAAG,IAAI,GAAG,SAAS,EAC/B,SAAS,MAAM,KACd,MAAM,GAAG,IAsCX,CAAA"}
@@ -0,0 +1,7 @@
1
+ export { getInAppOpenLinkTarget } from './getInAppOpenLinkTarget';
2
+ export { isAllowedIframeSrc } from './isAllowedIframeSrc';
3
+ export { isSafeNavigationHref } from './isSafeNavigationHref';
4
+ export { normalizeInAppLinks } from './normalizeInAppLinks';
5
+ export { openStaffbaseAware } from './openStaffbaseAware';
6
+ export { tryOpenWithStaffbase } from './tryOpenWithStaffbase';
7
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/links/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,sBAAsB,EAAE,MAAM,0BAA0B,CAAA;AACjE,OAAO,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAA;AACzD,OAAO,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAA;AAC7D,OAAO,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAA;AAC3D,OAAO,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAA;AACzD,OAAO,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAA"}
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Returns true when an iframe src may be rendered. Same-origin sources are
3
+ * always allowed; cross-origin sources must match the embed allowlist. Pair with
4
+ * sanitizeArticleHtml's `isAllowedIframeSrc` option.
5
+ * @param {string} src - The iframe src attribute value.
6
+ * @returns {boolean} True when the iframe may be kept.
7
+ */
8
+ export declare const isAllowedIframeSrc: (src: string) => boolean;
9
+ //# sourceMappingURL=isAllowedIframeSrc.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"isAllowedIframeSrc.d.ts","sourceRoot":"","sources":["../../../src/links/isAllowedIframeSrc.ts"],"names":[],"mappings":"AAeA;;;;;;GAMG;AACH,eAAO,MAAM,kBAAkB,GAAI,KAAK,MAAM,KAAG,OAqBhD,CAAA"}
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Type guard for Promise-like (thenable) values, used to normalize whatever
3
+ * Staffbase's openLink returns.
4
+ * @param {unknown} value - The value to test.
5
+ * @returns {boolean} True when value has a callable then method.
6
+ */
7
+ export declare const isPromiseLike: (value: unknown) => value is PromiseLike<unknown>;
8
+ //# sourceMappingURL=isPromiseLike.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"isPromiseLike.d.ts","sourceRoot":"","sources":["../../../src/links/isPromiseLike.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,eAAO,MAAM,aAAa,GAAI,OAAO,OAAO,KAAG,KAAK,IAAI,WAAW,CAAC,OAAO,CAIf,CAAA"}
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Returns true when an href is safe to pass to a real browser navigation
3
+ * (e.g. `window.location.assign`). Blocks scripted/inline schemes that could
4
+ * execute in the session-bearing webview. Relative URLs and http(s)/mailto/tel
5
+ * are allowed. Promoted from staffbase-alerts.
6
+ * @param {string} href - The href to validate.
7
+ * @returns {boolean} True when the href is safe to navigate to.
8
+ */
9
+ export declare const isSafeNavigationHref: (href: string) => boolean;
10
+ //# sourceMappingURL=isSafeNavigationHref.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"isSafeNavigationHref.d.ts","sourceRoot":"","sources":["../../../src/links/isSafeNavigationHref.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,eAAO,MAAM,oBAAoB,GAAI,MAAM,MAAM,KAAG,OASnD,CAAA"}
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Normalizes links inside Staffbase-rendered HTML so they behave better in mobile
3
+ * apps. On iOS, absolute same-origin links and/or `target="_blank"` can trigger
4
+ * Safari instead of in-app navigation; for same-origin links we rewrite to
5
+ * relative paths and remove target/rel. Ported from staffbase-alerts (the
6
+ * superset): cross-origin `target="_blank"` links are hardened with
7
+ * `rel="noopener noreferrer"` instead of left untouched. The Staffbase origin is
8
+ * passed in (the lib never reads env).
9
+ * @param {HTMLElement} root - The container whose anchors are normalized.
10
+ * @param {string} staffbaseOrigin - The Staffbase origin used to detect same-origin links.
11
+ * @returns {void}
12
+ */
13
+ export declare const normalizeInAppLinks: (root: HTMLElement, staffbaseOrigin: string) => void;
14
+ //# sourceMappingURL=normalizeInAppLinks.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"normalizeInAppLinks.d.ts","sourceRoot":"","sources":["../../../src/links/normalizeInAppLinks.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,mBAAmB,GAC9B,MAAM,WAAW,EACjB,iBAAiB,MAAM,KACtB,IAsDF,CAAA"}
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Try to open with Staffbase; if not available, navigate normally in the same
3
+ * tab. Ported from staffbase-alerts (the superset): it guards with
4
+ * isSafeNavigationHref first, so scripted schemes never reach navigation.
5
+ * @param {string} href - The target URL to open.
6
+ * @returns {boolean} True if Staffbase handled the navigation, otherwise false.
7
+ */
8
+ export declare const openStaffbaseAware: (href: string) => boolean;
9
+ //# sourceMappingURL=openStaffbaseAware.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"openStaffbaseAware.d.ts","sourceRoot":"","sources":["../../../src/links/openStaffbaseAware.ts"],"names":[],"mappings":"AAGA;;;;;;GAMG;AACH,eAAO,MAAM,kBAAkB,GAAI,MAAM,MAAM,KAAG,OAuDjD,CAAA"}
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Strips a leading Staffbase link prefix (`/deeplink/` or `/openlink/`) from a
3
+ * URL pathname, leaving the canonical in-app path.
4
+ * @param {string} path - The URL pathname.
5
+ * @returns {string} The path without the Staffbase prefix.
6
+ */
7
+ export declare const stripStaffbaseLinkPrefix: (path: string) => string;
8
+ //# sourceMappingURL=stripStaffbaseLinkPrefix.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stripStaffbaseLinkPrefix.d.ts","sourceRoot":"","sources":["../../../src/links/stripStaffbaseLinkPrefix.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,eAAO,MAAM,wBAAwB,GAAI,MAAM,MAAM,KAAG,MAMvD,CAAA"}
@@ -0,0 +1,10 @@
1
+ import { TryOpenResult } from '../types/links/TryOpenResult';
2
+ /**
3
+ * Attempt to open a link using Staffbase's plugin util.openLink if available.
4
+ * Ported from staffbase-alerts (typed Promise-like guard; smart-search used
5
+ * `as any`).
6
+ * @param {string} href - The target URL to open.
7
+ * @returns {TryOpenResult} Whether Staffbase handled it and a promise if available.
8
+ */
9
+ export declare const tryOpenWithStaffbase: (href: string) => TryOpenResult;
10
+ //# sourceMappingURL=tryOpenWithStaffbase.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tryOpenWithStaffbase.d.ts","sourceRoot":"","sources":["../../../src/links/tryOpenWithStaffbase.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,8BAA8B,CAAA;AAKjE;;;;;;GAMG;AACH,eAAO,MAAM,oBAAoB,GAAI,MAAM,MAAM,KAAG,aAoBnD,CAAA"}
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Result of attempting to open a link through Staffbase's plugin util.openLink.
3
+ */
4
+ export interface TryOpenResult {
5
+ /** Whether Staffbase handled the open call. */
6
+ handled: boolean;
7
+ /** The promise openLink returned, when it returned one. */
8
+ promise?: Promise<void>;
9
+ }
10
+ //# sourceMappingURL=TryOpenResult.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"TryOpenResult.d.ts","sourceRoot":"","sources":["../../../../src/types/links/TryOpenResult.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,+CAA+C;IAC/C,OAAO,EAAE,OAAO,CAAA;IAChB,2DAA2D;IAC3D,OAAO,CAAC,EAAE,OAAO,CAAC,IAAI,CAAC,CAAA;CACxB"}
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Window augmented with the Staffbase plugin util surface used for in-app link
3
+ * opening. The whole chain is optional because the host API may be absent.
4
+ */
5
+ export interface WindowWithStaffbase extends Window {
6
+ staffbase?: {
7
+ plugin?: {
8
+ util?: {
9
+ openLink?: (link: string, opts?: unknown) => unknown;
10
+ };
11
+ };
12
+ };
13
+ }
14
+ //# sourceMappingURL=WindowWithStaffbase.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"WindowWithStaffbase.d.ts","sourceRoot":"","sources":["../../../../src/types/links/WindowWithStaffbase.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,MAAM,WAAW,mBAAoB,SAAQ,MAAM;IACjD,SAAS,CAAC,EAAE;QACV,MAAM,CAAC,EAAE;YACP,IAAI,CAAC,EAAE;gBACL,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,KAAK,OAAO,CAAA;aACrD,CAAA;SACF,CAAA;KACF,CAAA;CACF"}
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Options for renderWidgets.
3
+ */
4
+ export interface RenderWidgetsOptions {
5
+ /** Maximum render attempts before giving up. Default 10. */
6
+ maxRetries?: number;
7
+ /** Delay between attempts, in milliseconds. Default 300. */
8
+ retryDelay?: number;
9
+ /**
10
+ * Called when an error is swallowed. `context` labels where it happened
11
+ * (e.g. 'manager-unavailable', 'extract', 'render-widget'). Default: warn on
12
+ * critical context only.
13
+ */
14
+ onError?: (error: unknown, context: string) => void;
15
+ /**
16
+ * Token used to dedupe/cancel repeated renders. A newer render with the same
17
+ * key cancels older in-flight/queued ones. Default: the container element.
18
+ */
19
+ cancelKey?: object;
20
+ }
21
+ //# sourceMappingURL=RenderWidgetsOptions.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"RenderWidgetsOptions.d.ts","sourceRoot":"","sources":["../../../../src/types/widgets/RenderWidgetsOptions.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,MAAM,WAAW,oBAAoB;IACnC,4DAA4D;IAC5D,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,4DAA4D;IAC5D,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB;;;;OAIG;IACH,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,KAAK,IAAI,CAAA;IACnD;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB"}
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Result of a renderWidgets call. `rendered` is the number of widgets rendered
3
+ * via the prototype path; it is 0 when the host constructor path handled
4
+ * rendering (the count is not observable there).
5
+ */
6
+ export type RenderWidgetsResult = {
7
+ ok: true;
8
+ rendered: number;
9
+ } | {
10
+ ok: false;
11
+ reason: 'no-container' | 'manager-unavailable' | 'no-widgets' | 'cancelled';
12
+ };
13
+ //# sourceMappingURL=RenderWidgetsResult.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"RenderWidgetsResult.d.ts","sourceRoot":"","sources":["../../../../src/types/widgets/RenderWidgetsResult.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,MAAM,MAAM,mBAAmB,GAC3B;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,GAC9B;IACE,EAAE,EAAE,KAAK,CAAA;IACT,MAAM,EACF,cAAc,GACd,qBAAqB,GACrB,YAAY,GACZ,WAAW,CAAA;CAChB,CAAA"}
@@ -0,0 +1,9 @@
1
+ /**
2
+ * The host's widget-manager constructor (`window.staffbase.content.widgetMgr`).
3
+ * The signature is internal; most builds accept `(unused, isEditor?)` and the
4
+ * instance exposes a `render(container)` method.
5
+ */
6
+ export type StaffbaseWidgetManagerConstructor = new (...args: unknown[]) => {
7
+ render?: (container: HTMLElement) => Promise<void> | void;
8
+ };
9
+ //# sourceMappingURL=StaffbaseWidgetManagerConstructor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"StaffbaseWidgetManagerConstructor.d.ts","sourceRoot":"","sources":["../../../../src/types/widgets/StaffbaseWidgetManagerConstructor.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,MAAM,MAAM,iCAAiC,GAAG,KAAK,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK;IAC1E,MAAM,CAAC,EAAE,CAAC,SAAS,EAAE,WAAW,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAA;CAC1D,CAAA"}
@@ -0,0 +1,11 @@
1
+ /**
2
+ * The host's private widget-manager prototype
3
+ * (`window.staffbase.content.widgetMgr.prototype`). All members are optional
4
+ * because this is an undocumented API that may change.
5
+ */
6
+ export interface StaffbaseWidgetManagerPrototype {
7
+ _widgets?: unknown[];
8
+ _extractWidgets?: (container: HTMLElement) => unknown[];
9
+ _renderWidget?: (this: StaffbaseWidgetManagerPrototype, container: HTMLElement, widget: unknown) => void;
10
+ }
11
+ //# sourceMappingURL=StaffbaseWidgetManagerPrototype.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"StaffbaseWidgetManagerPrototype.d.ts","sourceRoot":"","sources":["../../../../src/types/widgets/StaffbaseWidgetManagerPrototype.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,MAAM,WAAW,+BAA+B;IAC9C,QAAQ,CAAC,EAAE,OAAO,EAAE,CAAA;IACpB,eAAe,CAAC,EAAE,CAAC,SAAS,EAAE,WAAW,KAAK,OAAO,EAAE,CAAA;IACvD,aAAa,CAAC,EAAE,CACd,IAAI,EAAE,+BAA+B,EACrC,SAAS,EAAE,WAAW,EACtB,MAAM,EAAE,OAAO,KACZ,IAAI,CAAA;CACV"}
@@ -0,0 +1,16 @@
1
+ import { DependencyList } from 'react';
2
+ import { RenderWidgetsOptions } from './RenderWidgetsOptions';
3
+ /**
4
+ * Options for the useRenderWidgets hook: renderWidgets options plus optional
5
+ * MutationObserver wiring.
6
+ */
7
+ export interface UseRenderWidgetsOptions extends RenderWidgetsOptions {
8
+ /**
9
+ * When true, a debounced MutationObserver (childList + subtree) re-renders on
10
+ * article HTML changes (accordions, "View more"), cleaned up on unmount.
11
+ */
12
+ observe?: boolean;
13
+ /** Effect dependency list controlling when a render re-runs. Default []. */
14
+ deps?: DependencyList;
15
+ }
16
+ //# sourceMappingURL=UseRenderWidgetsOptions.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"UseRenderWidgetsOptions.d.ts","sourceRoot":"","sources":["../../../../src/types/widgets/UseRenderWidgetsOptions.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,OAAO,CAAA;AAE3C,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAA;AAElE;;;GAGG;AACH,MAAM,WAAW,uBAAwB,SAAQ,oBAAoB;IACnE;;;OAGG;IACH,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,4EAA4E;IAC5E,IAAI,CAAC,EAAE,cAAc,CAAA;CACtB"}
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Default onError for renderWidgets: warns only on the critical
3
+ * 'manager-unavailable' context so a platform regression stays visible in logs
4
+ * without spamming per-widget noise. Consumers can pass their own onError.
5
+ * @param {unknown} error - The swallowed error.
6
+ * @param {string} context - Where it happened.
7
+ * @returns {void}
8
+ */
9
+ export declare const defaultWidgetOnError: (error: unknown, context: string) => void;
10
+ //# sourceMappingURL=defaultWidgetOnError.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"defaultWidgetOnError.d.ts","sourceRoot":"","sources":["../../../src/widgets/defaultWidgetOnError.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,eAAO,MAAM,oBAAoB,GAAI,OAAO,OAAO,EAAE,SAAS,MAAM,KAAG,IAOtE,CAAA"}
@@ -0,0 +1,8 @@
1
+ import { StaffbaseWidgetManagerConstructor } from '../types/widgets/StaffbaseWidgetManagerConstructor';
2
+ /**
3
+ * Resolves the host widget-manager constructor from the global Staffbase object,
4
+ * or null when it is not a function (older runtimes / platform regression).
5
+ * @returns {StaffbaseWidgetManagerConstructor | null} The constructor or null.
6
+ */
7
+ export declare const getWidgetManagerConstructor: () => StaffbaseWidgetManagerConstructor | null;
8
+ //# sourceMappingURL=getWidgetManagerConstructor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"getWidgetManagerConstructor.d.ts","sourceRoot":"","sources":["../../../src/widgets/getWidgetManagerConstructor.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iCAAiC,EAAE,MAAM,oDAAoD,CAAA;AAE3G;;;;GAIG;AACH,eAAO,MAAM,2BAA2B,QAClC,iCAAiC,GAAG,IAUvC,CAAA"}
@@ -0,0 +1,8 @@
1
+ import { StaffbaseWidgetManagerPrototype } from '../types/widgets/StaffbaseWidgetManagerPrototype';
2
+ /**
3
+ * Resolves the host widget-manager prototype (the private fallback API) from the
4
+ * global Staffbase object, or null when it is unavailable.
5
+ * @returns {StaffbaseWidgetManagerPrototype | null} The prototype or null.
6
+ */
7
+ export declare const getWidgetManagerPrototype: () => StaffbaseWidgetManagerPrototype | null;
8
+ //# sourceMappingURL=getWidgetManagerPrototype.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"getWidgetManagerPrototype.d.ts","sourceRoot":"","sources":["../../../src/widgets/getWidgetManagerPrototype.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,+BAA+B,EAAE,MAAM,kDAAkD,CAAA;AAEvG;;;;GAIG;AACH,eAAO,MAAM,yBAAyB,QAChC,+BAA+B,GAAG,IAYrC,CAAA"}
@@ -0,0 +1,12 @@
1
+ import { StaffbaseWidgetManagerPrototype } from '../types/widgets/StaffbaseWidgetManagerPrototype';
2
+ /**
3
+ * Type guard asserting the widget-manager prototype exposes the methods the
4
+ * prototype render path needs.
5
+ * @param {StaffbaseWidgetManagerPrototype} widgetMgr - The resolved prototype.
6
+ * @returns {boolean} True when both _extractWidgets and _renderWidget are functions.
7
+ */
8
+ export declare const hasRequiredWidgetManagerMethods: (widgetMgr: StaffbaseWidgetManagerPrototype) => widgetMgr is StaffbaseWidgetManagerPrototype & {
9
+ _extractWidgets: (container: HTMLElement) => unknown[];
10
+ _renderWidget: (this: StaffbaseWidgetManagerPrototype, container: HTMLElement, widget: unknown) => void;
11
+ };
12
+ //# sourceMappingURL=hasRequiredWidgetManagerMethods.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hasRequiredWidgetManagerMethods.d.ts","sourceRoot":"","sources":["../../../src/widgets/hasRequiredWidgetManagerMethods.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,+BAA+B,EAAE,MAAM,kDAAkD,CAAA;AAEvG;;;;;GAKG;AACH,eAAO,MAAM,+BAA+B,GAC1C,WAAW,+BAA+B,KACzC,SAAS,IAAI,+BAA+B,GAAG;IAChD,eAAe,EAAE,CAAC,SAAS,EAAE,WAAW,KAAK,OAAO,EAAE,CAAA;IACtD,aAAa,EAAE,CACb,IAAI,EAAE,+BAA+B,EACrC,SAAS,EAAE,WAAW,EACtB,MAAM,EAAE,OAAO,KACZ,IAAI,CAAA;CAGoC,CAAA"}
@@ -0,0 +1,2 @@
1
+ export { renderWidgets } from './renderWidgets';
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/widgets/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAA"}
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Detects known, benign internal Staffbase errors thrown by `_renderWidget` for
3
+ * individual widgets, so they can be swallowed without aborting the batch.
4
+ * Promoted from unacknowledged-bulletins (`each` / `undefined is not an object`).
5
+ * @param {unknown} error - The thrown value.
6
+ * @returns {boolean} True when the error is a known internal render error.
7
+ */
8
+ export declare const isKnownStaffbaseRenderError: (error: unknown) => boolean;
9
+ //# sourceMappingURL=isKnownStaffbaseRenderError.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"isKnownStaffbaseRenderError.d.ts","sourceRoot":"","sources":["../../../src/widgets/isKnownStaffbaseRenderError.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,eAAO,MAAM,2BAA2B,GAAI,OAAO,OAAO,KAAG,OAGH,CAAA"}
@@ -0,0 +1,2 @@
1
+ export { useRenderWidgets } from './useRenderWidgets';
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/widgets/react/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAA"}
@@ -0,0 +1,14 @@
1
+ import { RefObject } from 'react';
2
+ import { UseRenderWidgetsOptions } from '../../types/widgets/UseRenderWidgetsOptions';
3
+ /**
4
+ * React adapter for renderWidgets. Renders on mount and whenever `deps` change,
5
+ * using `ref.current` as the cancelKey. With `observe: true` it mounts a
6
+ * debounced MutationObserver (childList + subtree) that re-renders on article
7
+ * HTML changes and cleans up on unmount. Absorbs the render/observe glue that
8
+ * the widgets repeat in their Article loaders.
9
+ * @param {RefObject<HTMLElement | null>} ref - Ref to the widget container.
10
+ * @param {UseRenderWidgetsOptions} options - renderWidgets options plus observe/deps.
11
+ * @returns {void}
12
+ */
13
+ export declare const useRenderWidgets: (ref: RefObject<HTMLElement | null>, options?: UseRenderWidgetsOptions) => void;
14
+ //# sourceMappingURL=useRenderWidgets.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useRenderWidgets.d.ts","sourceRoot":"","sources":["../../../../src/widgets/react/useRenderWidgets.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAGtC,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,6CAA6C,CAAA;AAG1F;;;;;;;;;GASG;AACH,eAAO,MAAM,gBAAgB,GAC3B,KAAK,SAAS,CAAC,WAAW,GAAG,IAAI,CAAC,EAClC,UAAS,uBAA4B,KACpC,IAwBF,CAAA"}
@@ -0,0 +1,16 @@
1
+ import { RenderWidgetsOptions } from '../types/widgets/RenderWidgetsOptions';
2
+ import { RenderWidgetsResult } from '../types/widgets/RenderWidgetsResult';
3
+ /**
4
+ * Renders the widgets embedded in `container` using the host's private widget
5
+ * manager. Superset of the four widget services: constructor path first with a
6
+ * prototype fallback, a global queue that serializes renders, per-cancelKey
7
+ * cancellation of superseded renders, configurable retries, swallowing of known
8
+ * internal Staffbase errors, and a once-per-session warning when the manager is
9
+ * missing. Returns a typed result so callers can react (e.g. mark a
10
+ * data-widget-render-error attribute).
11
+ * @param {HTMLElement | null} container - The element whose embedded widgets are rendered.
12
+ * @param {RenderWidgetsOptions} options - Retry, error and cancellation options.
13
+ * @returns {Promise<RenderWidgetsResult>} The typed render result.
14
+ */
15
+ export declare const renderWidgets: (container: HTMLElement | null, options?: RenderWidgetsOptions) => Promise<RenderWidgetsResult>;
16
+ //# sourceMappingURL=renderWidgets.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"renderWidgets.d.ts","sourceRoot":"","sources":["../../../src/widgets/renderWidgets.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,uCAAuC,CAAA;AACjF,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,sCAAsC,CAAA;AA8G/E;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,aAAa,GACxB,WAAW,WAAW,GAAG,IAAI,EAC7B,UAAS,oBAAyB,KACjC,OAAO,CAAC,mBAAmB,CAwB7B,CAAA"}
@@ -0,0 +1,2 @@
1
+ Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("../renderWidgets-EbJq9Wn6.js");let t=require("react");var n=(n,r={})=>{let{observe:i=!1,deps:a=[],...o}=r;(0,t.useEffect)(()=>{let t=n.current;if(!t)return;let r={...o,cancelKey:t};if(e.t(t,r),!i)return;let a,s=new MutationObserver(()=>{clearTimeout(a),a=setTimeout(()=>void e.t(n.current,r),100)});return s.observe(t,{childList:!0,subtree:!0}),()=>{clearTimeout(a),s.disconnect()}},a)};exports.useRenderWidgets=n;
2
+ //# sourceMappingURL=react.cjs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"react.cjs.js","names":[],"sources":["../../src/widgets/react/useRenderWidgets.ts"],"sourcesContent":["import type { RefObject } from 'react'\nimport { useEffect } from 'react'\n\nimport type { UseRenderWidgetsOptions } from '../../types/widgets/UseRenderWidgetsOptions'\nimport { renderWidgets } from '../renderWidgets'\n\n/**\n * React adapter for renderWidgets. Renders on mount and whenever `deps` change,\n * using `ref.current` as the cancelKey. With `observe: true` it mounts a\n * debounced MutationObserver (childList + subtree) that re-renders on article\n * HTML changes and cleans up on unmount. Absorbs the render/observe glue that\n * the widgets repeat in their Article loaders.\n * @param {RefObject<HTMLElement | null>} ref - Ref to the widget container.\n * @param {UseRenderWidgetsOptions} options - renderWidgets options plus observe/deps.\n * @returns {void}\n */\nexport const useRenderWidgets = (\n ref: RefObject<HTMLElement | null>,\n options: UseRenderWidgetsOptions = {},\n): void => {\n const { observe = false, deps = [], ...renderOptions } = options\n\n useEffect(() => {\n const el = ref.current\n if (!el) return\n\n const opts = { ...renderOptions, cancelKey: el }\n void renderWidgets(el, opts)\n\n if (!observe) return\n\n let timer: ReturnType<typeof setTimeout>\n const observer = new MutationObserver(() => {\n clearTimeout(timer)\n timer = setTimeout(() => void renderWidgets(ref.current, opts), 100)\n })\n observer.observe(el, { childList: true, subtree: true })\n\n return () => {\n clearTimeout(timer)\n observer.disconnect()\n }\n }, deps)\n}\n"],"mappings":"0IAgBA,IAAa,GACX,EACA,EAAmC,CAAC,IAC3B,CACT,GAAM,CAAE,UAAU,GAAO,OAAO,CAAC,EAAG,GAAG,GAAkB,GAEzD,EAAA,EAAA,eAAgB,CACd,IAAM,EAAK,EAAI,QACf,GAAI,CAAC,EAAI,OAET,IAAM,EAAO,CAAE,GAAG,EAAe,UAAW,CAAG,EAG/C,GAFA,EAAK,EAAc,EAAI,CAAI,EAEvB,CAAC,EAAS,OAEd,IAAI,EACE,EAAW,IAAI,qBAAuB,CAC1C,aAAa,CAAK,EAClB,EAAQ,eAAiB,KAAK,EAAA,EAAc,EAAI,QAAS,CAAI,EAAG,GAAG,CACrE,CAAC,EAGD,OAFA,EAAS,QAAQ,EAAI,CAAE,UAAW,GAAM,QAAS,EAAK,CAAC,MAE1C,CACX,aAAa,CAAK,EAClB,EAAS,WAAW,CACtB,CACF,EAAG,CAAI,CACT"}
@@ -0,0 +1,28 @@
1
+ import { t as e } from "../renderWidgets-CeIczubt.mjs";
2
+ import { useEffect as t } from "react";
3
+ //#region src/widgets/react/useRenderWidgets.ts
4
+ var n = (n, r = {}) => {
5
+ let { observe: i = !1, deps: a = [], ...o } = r;
6
+ t(() => {
7
+ let t = n.current;
8
+ if (!t) return;
9
+ let r = {
10
+ ...o,
11
+ cancelKey: t
12
+ };
13
+ if (e(t, r), !i) return;
14
+ let a, s = new MutationObserver(() => {
15
+ clearTimeout(a), a = setTimeout(() => void e(n.current, r), 100);
16
+ });
17
+ return s.observe(t, {
18
+ childList: !0,
19
+ subtree: !0
20
+ }), () => {
21
+ clearTimeout(a), s.disconnect();
22
+ };
23
+ }, a);
24
+ };
25
+ //#endregion
26
+ export { n as useRenderWidgets };
27
+
28
+ //# sourceMappingURL=react.es.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"react.es.mjs","names":[],"sources":["../../src/widgets/react/useRenderWidgets.ts"],"sourcesContent":["import type { RefObject } from 'react'\nimport { useEffect } from 'react'\n\nimport type { UseRenderWidgetsOptions } from '../../types/widgets/UseRenderWidgetsOptions'\nimport { renderWidgets } from '../renderWidgets'\n\n/**\n * React adapter for renderWidgets. Renders on mount and whenever `deps` change,\n * using `ref.current` as the cancelKey. With `observe: true` it mounts a\n * debounced MutationObserver (childList + subtree) that re-renders on article\n * HTML changes and cleans up on unmount. Absorbs the render/observe glue that\n * the widgets repeat in their Article loaders.\n * @param {RefObject<HTMLElement | null>} ref - Ref to the widget container.\n * @param {UseRenderWidgetsOptions} options - renderWidgets options plus observe/deps.\n * @returns {void}\n */\nexport const useRenderWidgets = (\n ref: RefObject<HTMLElement | null>,\n options: UseRenderWidgetsOptions = {},\n): void => {\n const { observe = false, deps = [], ...renderOptions } = options\n\n useEffect(() => {\n const el = ref.current\n if (!el) return\n\n const opts = { ...renderOptions, cancelKey: el }\n void renderWidgets(el, opts)\n\n if (!observe) return\n\n let timer: ReturnType<typeof setTimeout>\n const observer = new MutationObserver(() => {\n clearTimeout(timer)\n timer = setTimeout(() => void renderWidgets(ref.current, opts), 100)\n })\n observer.observe(el, { childList: true, subtree: true })\n\n return () => {\n clearTimeout(timer)\n observer.disconnect()\n }\n }, deps)\n}\n"],"mappings":";;;AAgBA,IAAa,KACX,GACA,IAAmC,CAAC,MAC3B;CACT,IAAM,EAAE,aAAU,IAAO,UAAO,CAAC,GAAG,GAAG,MAAkB;CAEzD,QAAgB;EACd,IAAM,IAAK,EAAI;EACf,IAAI,CAAC,GAAI;EAET,IAAM,IAAO;GAAE,GAAG;GAAe,WAAW;EAAG;EAG/C,IAFA,EAAmB,GAAI,CAAI,GAEvB,CAAC,GAAS;EAEd,IAAI,GACE,IAAW,IAAI,uBAAuB;GAE1C,AADA,aAAa,CAAK,GAClB,IAAQ,iBAAiB,KAAK,EAAc,EAAI,SAAS,CAAI,GAAG,GAAG;EACrE,CAAC;EAGD,OAFA,EAAS,QAAQ,GAAI;GAAE,WAAW;GAAM,SAAS;EAAK,CAAC,SAE1C;GAEX,AADA,aAAa,CAAK,GAClB,EAAS,WAAW;EACtB;CACF,GAAG,CAAI;AACT"}
@@ -0,0 +1 @@
1
+ Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("./renderWidgets-EbJq9Wn6.js");exports.renderWidgets=e.t;
@@ -0,0 +1,2 @@
1
+ import { t as e } from "./renderWidgets-CeIczubt.mjs";
2
+ export { e as renderWidgets };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@favish/staffbase-utils",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
4
4
  "description": "Shared internal/host utilities for Staffbase widgets",
5
5
  "author": "Favish <dev@favish.com>",
6
6
  "license": "UNLICENSED",
@@ -22,6 +22,21 @@
22
22
  "types": "./dist/src/html/index.d.ts",
23
23
  "import": "./dist/html.es.mjs",
24
24
  "require": "./dist/html.cjs.js"
25
+ },
26
+ "./links": {
27
+ "types": "./dist/src/links/index.d.ts",
28
+ "import": "./dist/links.es.mjs",
29
+ "require": "./dist/links.cjs.js"
30
+ },
31
+ "./widgets": {
32
+ "types": "./dist/src/widgets/index.d.ts",
33
+ "import": "./dist/widgets.es.mjs",
34
+ "require": "./dist/widgets.cjs.js"
35
+ },
36
+ "./widgets/react": {
37
+ "types": "./dist/src/widgets/react/index.d.ts",
38
+ "import": "./dist/widgets/react.es.mjs",
39
+ "require": "./dist/widgets/react.cjs.js"
25
40
  }
26
41
  },
27
42
  "repository": {
@@ -50,6 +65,7 @@
50
65
  "@testing-library/jest-dom": "^6.9.1",
51
66
  "@types/jest": "^30.0.0",
52
67
  "@types/node": "^25",
68
+ "@types/react": "19.2.16",
53
69
  "@typescript-eslint/eslint-plugin": "^8.60.0",
54
70
  "@typescript-eslint/parser": "^8.60.0",
55
71
  "eslint": "^10.4.0",