@favish/staffbase-utils 0.13.0 → 0.14.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 (50) hide show
  1. package/README.md +2 -0
  2. package/dist/chunk-CMqjfN_6.js +1 -0
  3. package/dist/device.cjs.js.map +1 -1
  4. package/dist/device.es.mjs.map +1 -1
  5. package/dist/html.cjs.js +1 -1
  6. package/dist/html.cjs.js.map +1 -1
  7. package/dist/injectShadowStyles-C40qdwKI.mjs +15 -0
  8. package/dist/injectShadowStyles-C40qdwKI.mjs.map +1 -0
  9. package/dist/injectShadowStyles-SjxVBpXH.js +2 -0
  10. package/dist/injectShadowStyles-SjxVBpXH.js.map +1 -0
  11. package/dist/links/react.cjs.js.map +1 -1
  12. package/dist/links/react.es.mjs.map +1 -1
  13. package/dist/shadow/react.cjs.js +2 -0
  14. package/dist/shadow/react.cjs.js.map +1 -0
  15. package/dist/shadow/react.es.mjs +31 -0
  16. package/dist/shadow/react.es.mjs.map +1 -0
  17. package/dist/shadow.cjs.js +1 -0
  18. package/dist/shadow.es.mjs +2 -0
  19. package/dist/src/content/index.d.ts +1 -1
  20. package/dist/src/content/index.d.ts.map +1 -1
  21. package/dist/src/device/isMobile.d.ts.map +1 -1
  22. package/dist/src/links/react/index.d.ts +1 -1
  23. package/dist/src/links/react/index.d.ts.map +1 -1
  24. package/dist/src/shadow/index.d.ts +3 -0
  25. package/dist/src/shadow/index.d.ts.map +1 -0
  26. package/dist/src/shadow/injectShadowStyles.d.ts +14 -0
  27. package/dist/src/shadow/injectShadowStyles.d.ts.map +1 -0
  28. package/dist/src/shadow/isShadowRoot.d.ts +8 -0
  29. package/dist/src/shadow/isShadowRoot.d.ts.map +1 -0
  30. package/dist/src/shadow/react/PortalContainerContext.d.ts +6 -0
  31. package/dist/src/shadow/react/PortalContainerContext.d.ts.map +1 -0
  32. package/dist/src/shadow/react/PortalContainerProvider.d.ts +10 -0
  33. package/dist/src/shadow/react/PortalContainerProvider.d.ts.map +1 -0
  34. package/dist/src/shadow/react/ensureShadowMount.d.ts +15 -0
  35. package/dist/src/shadow/react/ensureShadowMount.d.ts.map +1 -0
  36. package/dist/src/shadow/react/getOrCreateDiv.d.ts +8 -0
  37. package/dist/src/shadow/react/getOrCreateDiv.d.ts.map +1 -0
  38. package/dist/src/shadow/react/getOrCreateEmotionInsertionPoint.d.ts +9 -0
  39. package/dist/src/shadow/react/getOrCreateEmotionInsertionPoint.d.ts.map +1 -0
  40. package/dist/src/shadow/react/index.d.ts +7 -0
  41. package/dist/src/shadow/react/index.d.ts.map +1 -0
  42. package/dist/src/shadow/react/usePortalContainer.d.ts +6 -0
  43. package/dist/src/shadow/react/usePortalContainer.d.ts.map +1 -0
  44. package/dist/src/types/shadow/EnsureShadowMountOptions.d.ts +14 -0
  45. package/dist/src/types/shadow/EnsureShadowMountOptions.d.ts.map +1 -0
  46. package/dist/src/types/shadow/PortalContainerProviderProps.d.ts +10 -0
  47. package/dist/src/types/shadow/PortalContainerProviderProps.d.ts.map +1 -0
  48. package/dist/src/types/shadow/ShadowMount.d.ts +13 -0
  49. package/dist/src/types/shadow/ShadowMount.d.ts.map +1 -0
  50. package/package.json +16 -1
package/README.md CHANGED
@@ -26,6 +26,8 @@ minimumReleaseAgeExclude:
26
26
  | `@favish/staffbase-utils/api` | `fetchJson`, `fetchAllPaginated`, `ApiError` |
27
27
  | `@favish/staffbase-utils/content` | `resolveLocalizedContent`, `resolveActiveLanguage`, `detectEditorLanguage`, `detectPreviewLanguage` |
28
28
  | `@favish/staffbase-utils/links/react` | `useInAppLinkHandling` (React peer) |
29
+ | `@favish/staffbase-utils/shadow` | `injectShadowStyles`, `isShadowRoot` |
30
+ | `@favish/staffbase-utils/shadow/react` | `ensureShadowMount`, `PortalContainerProvider`, `usePortalContainer` (React + @emotion/cache peers) |
29
31
  | `@favish/staffbase-utils/log` | `logError`, `logWarn`, `logDebug`, `setLoggingEnabled` |
30
32
  | `@favish/staffbase-utils/dom` | `getDynamicClasses` |
31
33
  | `@favish/staffbase-utils/types` | `Channel`, `ChannelLink`, `ChannelLinkParameter`, `DropdownOption`, `LocalizedContent`, `ArticleImage`, `ArticleImageVariant` (type-only) |
@@ -0,0 +1 @@
1
+ var e=Object.create,t=Object.defineProperty,n=Object.getOwnPropertyDescriptor,r=Object.getOwnPropertyNames,i=Object.getPrototypeOf,a=Object.prototype.hasOwnProperty,o=(e,i,o,s)=>{if(i&&typeof i==`object`||typeof i==`function`)for(var c=r(i),l=0,u=c.length,d;l<u;l++)d=c[l],!a.call(e,d)&&d!==o&&t(e,d,{get:(e=>i[e]).bind(null,d),enumerable:!(s=n(i,d))||s.enumerable});return e},s=(n,r,a)=>(a=n==null?{}:e(i(n)),o(r||!n||!n.__esModule?t(a,`default`,{value:n,enumerable:!0}):a,n));Object.defineProperty(exports,"t",{enumerable:!0,get:function(){return s}});
@@ -1 +1 @@
1
- {"version":3,"file":"device.cjs.js","names":[],"sources":["../src/device/isMobileOrWebview.ts","../src/device/isMobileViewport.ts","../src/device/isMobile.ts"],"sourcesContent":["/**\n * Detects if the current environment is a mobile device or running inside a\n * webview, using userAgent heuristics common on iOS/Android and webviews.\n * Guards against non-browser environments (SSR/tests without navigator).\n * @returns {boolean} True if likely mobile or webview.\n */\nexport const isMobileOrWebview = (): boolean => {\n if (typeof navigator === 'undefined') return false\n\n const userAgent = navigator.userAgent.toLowerCase()\n const isMobilePlatform =\n /mobile|android|ios|iphone|ipad|ipod|windows phone/i.test(userAgent)\n const isWebview = /wv|webview/i.test(userAgent)\n\n return isMobilePlatform || isWebview\n}\n","/**\n * Checks if the viewport width is mobile-sized (<= 768px).\n * Guards against non-browser environments (SSR/tests without window).\n * @returns {boolean} True if the viewport width is 768px or less.\n */\nexport const isMobileViewport = (): boolean => {\n if (typeof window === 'undefined') return false\n return window.innerWidth <= 768\n}\n","import { isMobileOrWebview } from './isMobileOrWebview'\nimport { isMobileViewport } from './isMobileViewport'\n\n/**\n * Checks if the device is mobile either by user agent/webview or viewport size.\n * @returns {boolean} True if either a mobile/webview UA or a mobile viewport.\n */\nexport const isMobile = (): boolean =>\n isMobileOrWebview() || isMobileViewport()\n"],"mappings":"mEAMA,IAAa,MAAmC,CAC9C,GAAI,OAAO,UAAc,IAAa,MAAO,GAE7C,IAAM,EAAY,UAAU,UAAU,YAAY,EAC5C,EACJ,qDAAqD,KAAK,CAAS,EAC/D,EAAY,cAAc,KAAK,CAAS,EAE9C,OAAO,GAAoB,CAC7B,ECVa,MACP,OAAO,OAAW,IAAoB,GACnC,OAAO,YAAc,ICAjB,MACX,EAAkB,GAAK,EAAiB"}
1
+ {"version":3,"file":"device.cjs.js","names":[],"sources":["../src/device/isMobileOrWebview.ts","../src/device/isMobileViewport.ts","../src/device/isMobile.ts"],"sourcesContent":["/**\n * Detects if the current environment is a mobile device or running inside a\n * webview, using userAgent heuristics common on iOS/Android and webviews.\n * Guards against non-browser environments (SSR/tests without navigator).\n * @returns {boolean} True if likely mobile or webview.\n */\nexport const isMobileOrWebview = (): boolean => {\n if (typeof navigator === 'undefined') return false\n\n const userAgent = navigator.userAgent.toLowerCase()\n const isMobilePlatform =\n /mobile|android|ios|iphone|ipad|ipod|windows phone/i.test(userAgent)\n const isWebview = /wv|webview/i.test(userAgent)\n\n return isMobilePlatform || isWebview\n}\n","/**\n * Checks if the viewport width is mobile-sized (<= 768px).\n * Guards against non-browser environments (SSR/tests without window).\n * @returns {boolean} True if the viewport width is 768px or less.\n */\nexport const isMobileViewport = (): boolean => {\n if (typeof window === 'undefined') return false\n return window.innerWidth <= 768\n}\n","import { isMobileOrWebview } from './isMobileOrWebview'\nimport { isMobileViewport } from './isMobileViewport'\n\n/**\n * Checks if the device is mobile either by user agent/webview or viewport size.\n * @returns {boolean} True if either a mobile/webview UA or a mobile viewport.\n */\nexport const isMobile = (): boolean => isMobileOrWebview() || isMobileViewport()\n"],"mappings":"mEAMA,IAAa,MAAmC,CAC9C,GAAI,OAAO,UAAc,IAAa,MAAO,GAE7C,IAAM,EAAY,UAAU,UAAU,YAAY,EAC5C,EACJ,qDAAqD,KAAK,CAAS,EAC/D,EAAY,cAAc,KAAK,CAAS,EAE9C,OAAO,GAAoB,CAC7B,ECVa,MACP,OAAO,OAAW,IAAoB,GACnC,OAAO,YAAc,ICAjB,MAA0B,EAAkB,GAAK,EAAiB"}
@@ -1 +1 @@
1
- {"version":3,"file":"device.es.mjs","names":[],"sources":["../src/device/isMobileOrWebview.ts","../src/device/isMobileViewport.ts","../src/device/isMobile.ts"],"sourcesContent":["/**\n * Detects if the current environment is a mobile device or running inside a\n * webview, using userAgent heuristics common on iOS/Android and webviews.\n * Guards against non-browser environments (SSR/tests without navigator).\n * @returns {boolean} True if likely mobile or webview.\n */\nexport const isMobileOrWebview = (): boolean => {\n if (typeof navigator === 'undefined') return false\n\n const userAgent = navigator.userAgent.toLowerCase()\n const isMobilePlatform =\n /mobile|android|ios|iphone|ipad|ipod|windows phone/i.test(userAgent)\n const isWebview = /wv|webview/i.test(userAgent)\n\n return isMobilePlatform || isWebview\n}\n","/**\n * Checks if the viewport width is mobile-sized (<= 768px).\n * Guards against non-browser environments (SSR/tests without window).\n * @returns {boolean} True if the viewport width is 768px or less.\n */\nexport const isMobileViewport = (): boolean => {\n if (typeof window === 'undefined') return false\n return window.innerWidth <= 768\n}\n","import { isMobileOrWebview } from './isMobileOrWebview'\nimport { isMobileViewport } from './isMobileViewport'\n\n/**\n * Checks if the device is mobile either by user agent/webview or viewport size.\n * @returns {boolean} True if either a mobile/webview UA or a mobile viewport.\n */\nexport const isMobile = (): boolean =>\n isMobileOrWebview() || isMobileViewport()\n"],"mappings":";AAMA,IAAa,UAAmC;CAC9C,IAAI,OAAO,YAAc,KAAa,OAAO;CAE7C,IAAM,IAAY,UAAU,UAAU,YAAY,GAC5C,IACJ,qDAAqD,KAAK,CAAS,GAC/D,IAAY,cAAc,KAAK,CAAS;CAE9C,OAAO,KAAoB;AAC7B,GCVa,UACP,OAAO,SAAW,MAAoB,KACnC,OAAO,cAAc,KCAjB,UACX,EAAkB,KAAK,EAAiB"}
1
+ {"version":3,"file":"device.es.mjs","names":[],"sources":["../src/device/isMobileOrWebview.ts","../src/device/isMobileViewport.ts","../src/device/isMobile.ts"],"sourcesContent":["/**\n * Detects if the current environment is a mobile device or running inside a\n * webview, using userAgent heuristics common on iOS/Android and webviews.\n * Guards against non-browser environments (SSR/tests without navigator).\n * @returns {boolean} True if likely mobile or webview.\n */\nexport const isMobileOrWebview = (): boolean => {\n if (typeof navigator === 'undefined') return false\n\n const userAgent = navigator.userAgent.toLowerCase()\n const isMobilePlatform =\n /mobile|android|ios|iphone|ipad|ipod|windows phone/i.test(userAgent)\n const isWebview = /wv|webview/i.test(userAgent)\n\n return isMobilePlatform || isWebview\n}\n","/**\n * Checks if the viewport width is mobile-sized (<= 768px).\n * Guards against non-browser environments (SSR/tests without window).\n * @returns {boolean} True if the viewport width is 768px or less.\n */\nexport const isMobileViewport = (): boolean => {\n if (typeof window === 'undefined') return false\n return window.innerWidth <= 768\n}\n","import { isMobileOrWebview } from './isMobileOrWebview'\nimport { isMobileViewport } from './isMobileViewport'\n\n/**\n * Checks if the device is mobile either by user agent/webview or viewport size.\n * @returns {boolean} True if either a mobile/webview UA or a mobile viewport.\n */\nexport const isMobile = (): boolean => isMobileOrWebview() || isMobileViewport()\n"],"mappings":";AAMA,IAAa,UAAmC;CAC9C,IAAI,OAAO,YAAc,KAAa,OAAO;CAE7C,IAAM,IAAY,UAAU,UAAU,YAAY,GAC5C,IACJ,qDAAqD,KAAK,CAAS,GAC/D,IAAY,cAAc,KAAK,CAAS;CAE9C,OAAO,KAAoB;AAC7B,GCVa,UACP,OAAO,SAAW,MAAoB,KACnC,OAAO,cAAc,KCAjB,UAA0B,EAAkB,KAAK,EAAiB"}
package/dist/html.cjs.js CHANGED
@@ -1,2 +1,2 @@
1
- Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});var e=Object.create,t=Object.defineProperty,n=Object.getOwnPropertyDescriptor,r=Object.getOwnPropertyNames,i=Object.getPrototypeOf,a=Object.prototype.hasOwnProperty,o=(e,i,o,s)=>{if(i&&typeof i==`object`||typeof i==`function`)for(var c=r(i),l=0,u=c.length,d;l<u;l++)d=c[l],!a.call(e,d)&&d!==o&&t(e,d,{get:(e=>i[e]).bind(null,d),enumerable:!(s=n(i,d))||s.enumerable});return e},s=(n,r,a)=>(a=n==null?{}:e(i(n)),o(r||!n||!n.__esModule?t(a,`default`,{value:n,enumerable:!0}):a,n));let c=require("dompurify");c=s(c);var l=e=>{if(!e)return``;try{return c.default.sanitize(e,{ALLOWED_TAGS:[],KEEP_CONTENT:!0})}catch{return e.replace(/<\/?[^>]+(>|$)/g,``)}},u=e=>l(e).toLowerCase().replace(/[.,!?;:"'()[\]\-_/\\]/g,` `).replace(/\s+/g,` `).trim(),d={tagNameCheck:/^[a-z][a-z0-9]*-[a-z0-9-]*$/,attributeNameCheck:/^(?!on)[a-z][a-z0-9]*([-:][a-z0-9]+)*$/,allowCustomizedBuiltInElements:!0},f=e=>{let t=[];for(let n of e){if(!n||typeof n!=`object`||!(`element`in n))continue;let e=n.element;if(e instanceof Element){let n=e.tagName.toLowerCase();n.includes(`-`)&&t.push(n)}}t.length>0&&console.warn(`[staffbase-utils] sanitizer removed ${t.length} possible widget embed(s): ${t.join(`, `)} — they will not render. If these are valid embeds, the sanitizer config may need updating.`)},p=(0,c.default)(window),m=null;p.addHook(`afterSanitizeAttributes`,e=>{if(e instanceof Element&&(e.tagName===`A`&&e.getAttribute(`target`)===`_blank`&&e.setAttribute(`rel`,`noopener noreferrer`),e.tagName===`IFRAME`&&m)){let t=e.getAttribute(`src`)??``;m(t)||e.parentNode?.removeChild(e)}});var h=(e,t={})=>{m=t.isAllowedIframeSrc??null;try{let t=p.sanitize(e,{USE_PROFILES:{html:!0},CUSTOM_ELEMENT_HANDLING:d,ADD_TAGS:[`iframe`],ADD_ATTR:[`target`,`allow`,`allowfullscreen`,`frameborder`,`scrolling`,`loading`,`referrerpolicy`],FORBID_TAGS:[`script`,`style`],FORBID_ATTR:[`onerror`,`onload`,`onclick`]});return f(p.removed),t}finally{m=null}},g=e=>{let t=c.default.sanitize(e,{CUSTOM_ELEMENT_HANDLING:d});return f(c.default.removed),t};exports.cleanHTML=u,exports.sanitizeArticleHtml=h,exports.sanitizeHtml=g,exports.stripHtmlTags=l;
1
+ Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("./chunk-CMqjfN_6.js");let t=require("dompurify");t=e.t(t);var n=e=>{if(!e)return``;try{return t.default.sanitize(e,{ALLOWED_TAGS:[],KEEP_CONTENT:!0})}catch{return e.replace(/<\/?[^>]+(>|$)/g,``)}},r=e=>n(e).toLowerCase().replace(/[.,!?;:"'()[\]\-_/\\]/g,` `).replace(/\s+/g,` `).trim(),i={tagNameCheck:/^[a-z][a-z0-9]*-[a-z0-9-]*$/,attributeNameCheck:/^(?!on)[a-z][a-z0-9]*([-:][a-z0-9]+)*$/,allowCustomizedBuiltInElements:!0},a=e=>{let t=[];for(let n of e){if(!n||typeof n!=`object`||!(`element`in n))continue;let e=n.element;if(e instanceof Element){let n=e.tagName.toLowerCase();n.includes(`-`)&&t.push(n)}}t.length>0&&console.warn(`[staffbase-utils] sanitizer removed ${t.length} possible widget embed(s): ${t.join(`, `)} — they will not render. If these are valid embeds, the sanitizer config may need updating.`)},o=(0,t.default)(window),s=null;o.addHook(`afterSanitizeAttributes`,e=>{if(e instanceof Element&&(e.tagName===`A`&&e.getAttribute(`target`)===`_blank`&&e.setAttribute(`rel`,`noopener noreferrer`),e.tagName===`IFRAME`&&s)){let t=e.getAttribute(`src`)??``;s(t)||e.parentNode?.removeChild(e)}});var c=(e,t={})=>{s=t.isAllowedIframeSrc??null;try{let t=o.sanitize(e,{USE_PROFILES:{html:!0},CUSTOM_ELEMENT_HANDLING:i,ADD_TAGS:[`iframe`],ADD_ATTR:[`target`,`allow`,`allowfullscreen`,`frameborder`,`scrolling`,`loading`,`referrerpolicy`],FORBID_TAGS:[`script`,`style`],FORBID_ATTR:[`onerror`,`onload`,`onclick`]});return a(o.removed),t}finally{s=null}},l=e=>{let n=t.default.sanitize(e,{CUSTOM_ELEMENT_HANDLING:i});return a(t.default.removed),n};exports.cleanHTML=r,exports.sanitizeArticleHtml=c,exports.sanitizeHtml=l,exports.stripHtmlTags=n;
2
2
  //# sourceMappingURL=html.cjs.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"html.cjs.js","names":[],"sources":["../src/html/stripHtmlTags.ts","../src/html/cleanHTML.ts","../src/html/customElementHandling.ts","../src/html/logRemovedEmbeds.ts","../src/html/sanitizeArticleHtml.ts","../src/html/sanitizeHtml.ts"],"sourcesContent":["import DOMPurify from 'dompurify'\n\n/**\n * Strips all HTML tags, keeping the text content. From smart-search's\n * stripHtmlTags; its html-react-parser fallback is intentionally dropped to\n * avoid a heavy runtime dependency — the DOMPurify path plus a regex fallback is\n * sufficient and never throws in practice.\n * @param {string | undefined} html - String that may contain HTML tags.\n * @returns {string} The text content with tags removed.\n */\nexport const stripHtmlTags = (html?: string): string => {\n if (!html) return ''\n\n try {\n return DOMPurify.sanitize(html, { ALLOWED_TAGS: [], KEEP_CONTENT: true })\n } catch {\n return html.replace(/<\\/?[^>]+(>|$)/g, '')\n }\n}\n","import { stripHtmlTags } from './stripHtmlTags'\n\n/**\n * Normalizes HTML into a lowercase, punctuation-free, single-spaced token string\n * for search indexing/matching. From alerts' cleanHTML; builds on stripHtmlTags\n * so tag removal stays single-sourced.\n * @param {string} html - The HTML string to clean.\n * @returns {string} The cleaned, normalized text.\n */\nexport const cleanHTML = (html: string): string =>\n stripHtmlTags(html)\n .toLowerCase()\n .replace(/[.,!?;:\"'()[\\]\\-_/\\\\]/g, ' ')\n .replace(/\\s+/g, ' ')\n .trim()\n","import type { Config } from 'dompurify'\n\n/**\n * DOMPurify custom-element handling that preserves embedded-widget custom\n * elements (any hyphenated tag, e.g. `<news-teaser>`) and their kebab-case /\n * `data-` attributes so the host widget manager (renderWidgets) can hydrate\n * them. Without this, DOMPurify's default profile silently strips unknown custom\n * elements AND their attributes, wiping embeds out of the content before they can\n * render. `on*` event-handler attributes are still rejected (defense in depth\n * alongside DOMPurify's own XSS stripping). Promoted from staffbase-global-content.\n */\nexport const customElementHandling: NonNullable<\n Config['CUSTOM_ELEMENT_HANDLING']\n> = {\n // Any valid custom-element tag name (must contain a hyphen per the spec).\n tagNameCheck: /^[a-z][a-z0-9]*-[a-z0-9-]*$/,\n // Allow kebab-case / data- attributes on custom elements, but never `on*`.\n attributeNameCheck: /^(?!on)[a-z][a-z0-9]*([-:][a-z0-9]+)*$/,\n // Permit `<div is=\"some-widget\">`-style customized built-in elements.\n allowCustomizedBuiltInElements: true,\n}\n","/**\n * Warns (once per call) when the sanitizer dropped what looks like an embedded\n * widget — a custom-element tag (any hyphenated tag name). These removals are the\n * silent failure mode that makes embedded widgets \"disappear\" before the host can\n * render them, so surfacing them aids diagnosis. Scripts, styles and on*\n * handlers are intentionally removed and are NOT reported (they would be noise).\n * @param {readonly unknown[]} removed - DOMPurify's `removed` array after sanitize.\n * @returns {void}\n */\nexport const logRemovedEmbeds = (removed: readonly unknown[]): void => {\n const tags: string[] = []\n\n for (const entry of removed) {\n if (!entry || typeof entry !== 'object' || !('element' in entry)) continue\n const element = (entry as { element: unknown }).element\n if (element instanceof Element) {\n const tag = element.tagName.toLowerCase()\n if (tag.includes('-')) tags.push(tag)\n }\n }\n\n if (tags.length > 0) {\n console.warn(\n `[staffbase-utils] sanitizer removed ${tags.length} possible widget embed(s): ${tags.join(', ')} — they will not render. If these are valid embeds, the sanitizer config may need updating.`,\n )\n }\n}\n","import createDOMPurify from 'dompurify'\n\nimport type { SanitizeArticleHtmlOptions } from '../types/html/SanitizeArticleHtmlOptions'\nimport { customElementHandling } from './customElementHandling'\nimport { logRemovedEmbeds } from './logRemovedEmbeds'\n\n// Dedicated DOMPurify instance so the hardening hook below is scoped to this\n// sanitizer and never pollutes the consumer's shared default DOMPurify instance\n// (other code may sanitize through the default instance directly).\nconst purifier = createDOMPurify(window)\n\n// Set synchronously around each (synchronous) sanitize call. JS is\n// single-threaded, so the guard cannot leak across calls.\nlet activeIframeGuard: ((src: string) => boolean) | null = null\n\n// afterSanitizeAttributes hook: force rel=\"noopener noreferrer\" on\n// target=\"_blank\" anchors (reverse-tabnabbing), and drop iframes whose src fails\n// the injected allowlist predicate when one is active.\npurifier.addHook('afterSanitizeAttributes', (node) => {\n if (!(node instanceof Element)) return\n\n if (node.tagName === 'A' && node.getAttribute('target') === '_blank') {\n node.setAttribute('rel', 'noopener noreferrer')\n }\n\n if (node.tagName === 'IFRAME' && activeIframeGuard) {\n const src = node.getAttribute('src') ?? ''\n if (!activeIframeGuard(src)) node.parentNode?.removeChild(node)\n }\n})\n\n/**\n * Canonical sanitizer for rich article HTML rendered into a session-bearing\n * webview. Superset of the alerts and unacknowledged-bulletins variants: keeps\n * iframes and data-* attributes (renderWidgets discovers embedded sub-widgets via\n * data-*), strips scripts/inline handlers/javascript:, and forces\n * rel=\"noopener noreferrer\" on target=\"_blank\" anchors. When options.isAllowedIframeSrc\n * is provided, iframes whose src fails it are dropped (injected so this module\n * does not depend on /links, which owns isAllowedIframeSrc).\n * @param {string} html - Raw article HTML from the Staffbase API.\n * @param {SanitizeArticleHtmlOptions} options - Optional iframe-src allowlist.\n * @returns {string} Sanitized HTML safe to inject/parse.\n */\nexport const sanitizeArticleHtml = (\n html: string,\n options: SanitizeArticleHtmlOptions = {},\n): string => {\n activeIframeGuard = options.isAllowedIframeSrc ?? null\n try {\n const clean = purifier.sanitize(html, {\n USE_PROFILES: { html: true },\n CUSTOM_ELEMENT_HANDLING: customElementHandling,\n ADD_TAGS: ['iframe'],\n ADD_ATTR: [\n 'target',\n 'allow',\n 'allowfullscreen',\n 'frameborder',\n 'scrolling',\n 'loading',\n 'referrerpolicy',\n ],\n FORBID_TAGS: ['script', 'style'],\n FORBID_ATTR: ['onerror', 'onload', 'onclick'],\n })\n logRemovedEmbeds(purifier.removed)\n return clean\n } finally {\n activeIframeGuard = null\n }\n}\n","import DOMPurify from 'dompurify'\n\nimport { customElementHandling } from './customElementHandling'\nimport { logRemovedEmbeds } from './logRemovedEmbeds'\n\n/**\n * Strict sanitizer for untrusted snippet/teaser HTML rendered through a\n * non-sanitizing parser (e.g. html-react-parser). The default DOMPurify profile\n * strips scripts, inline handlers and dangerous URLs AND drops iframes, keeping\n * only basic rich-text markup. Embedded-widget custom elements (and their\n * kebab/data attributes) are preserved so the host can hydrate them. From\n * global-content's sanitizeHtml.\n *\n * Use sanitizeArticleHtml instead when rendering full article bodies that must\n * keep iframes / data-* embeds.\n * @param {string} html - Raw HTML string from the API.\n * @returns {string} Sanitized HTML.\n */\nexport const sanitizeHtml = (html: string): string => {\n const clean = DOMPurify.sanitize(html, {\n CUSTOM_ELEMENT_HANDLING: customElementHandling,\n })\n logRemovedEmbeds(DOMPurify.removed)\n return clean\n}\n"],"mappings":"mkBAUA,IAAa,EAAiB,GAA0B,CACtD,GAAI,CAAC,EAAM,MAAO,GAElB,GAAI,CACF,OAAO,EAAA,QAAU,SAAS,EAAM,CAAE,aAAc,CAAC,EAAG,aAAc,EAAK,CAAC,CAC1E,MAAQ,CACN,OAAO,EAAK,QAAQ,kBAAmB,EAAE,CAC3C,CACF,ECTa,EAAa,GACxB,EAAc,CAAI,EACf,YAAY,EACZ,QAAQ,yBAA0B,GAAG,EACrC,QAAQ,OAAQ,GAAG,EACnB,KAAK,ECHG,EAET,CAEF,aAAc,8BAEd,mBAAoB,yCAEpB,+BAAgC,EAClC,ECXa,EAAoB,GAAsC,CACrE,IAAM,EAAiB,CAAC,EAExB,IAAK,IAAM,KAAS,EAAS,CAC3B,GAAI,CAAC,GAAS,OAAO,GAAU,UAAY,EAAE,YAAa,GAAQ,SAClE,IAAM,EAAW,EAA+B,QAChD,GAAI,aAAmB,QAAS,CAC9B,IAAM,EAAM,EAAQ,QAAQ,YAAY,EACpC,EAAI,SAAS,GAAG,GAAG,EAAK,KAAK,CAAG,CACtC,CACF,CAEI,EAAK,OAAS,GAChB,QAAQ,KACN,uCAAuC,EAAK,OAAO,6BAA6B,EAAK,KAAK,IAAI,EAAE,4FAClG,CAEJ,ECjBM,GAAA,EAAA,EAAA,SAA2B,MAAM,EAInC,EAAuD,KAK3D,EAAS,QAAQ,0BAA4B,GAAS,CAC9C,gBAAgB,UAElB,EAAK,UAAY,KAAO,EAAK,aAAa,QAAQ,IAAM,UAC1D,EAAK,aAAa,MAAO,qBAAqB,EAG5C,EAAK,UAAY,UAAY,GAAmB,CAClD,IAAM,EAAM,EAAK,aAAa,KAAK,GAAK,GACnC,EAAkB,CAAG,GAAG,EAAK,YAAY,YAAY,CAAI,CAChE,CACF,CAAC,EAcD,IAAa,GACX,EACA,EAAsC,CAAC,IAC5B,CACX,EAAoB,EAAQ,oBAAsB,KAClD,GAAI,CACF,IAAM,EAAQ,EAAS,SAAS,EAAM,CACpC,aAAc,CAAE,KAAM,EAAK,EAC3B,wBAAyB,EACzB,SAAU,CAAC,QAAQ,EACnB,SAAU,CACR,SACA,QACA,kBACA,cACA,YACA,UACA,gBACF,EACA,YAAa,CAAC,SAAU,OAAO,EAC/B,YAAa,CAAC,UAAW,SAAU,SAAS,CAC9C,CAAC,EAED,OADA,EAAiB,EAAS,OAAO,EAC1B,CACT,QAAU,CACR,EAAoB,IACtB,CACF,ECpDa,EAAgB,GAAyB,CACpD,IAAM,EAAQ,EAAA,QAAU,SAAS,EAAM,CACrC,wBAAyB,CAC3B,CAAC,EAED,OADA,EAAiB,EAAA,QAAU,OAAO,EAC3B,CACT"}
1
+ {"version":3,"file":"html.cjs.js","names":[],"sources":["../src/html/stripHtmlTags.ts","../src/html/cleanHTML.ts","../src/html/customElementHandling.ts","../src/html/logRemovedEmbeds.ts","../src/html/sanitizeArticleHtml.ts","../src/html/sanitizeHtml.ts"],"sourcesContent":["import DOMPurify from 'dompurify'\n\n/**\n * Strips all HTML tags, keeping the text content. From smart-search's\n * stripHtmlTags; its html-react-parser fallback is intentionally dropped to\n * avoid a heavy runtime dependency — the DOMPurify path plus a regex fallback is\n * sufficient and never throws in practice.\n * @param {string | undefined} html - String that may contain HTML tags.\n * @returns {string} The text content with tags removed.\n */\nexport const stripHtmlTags = (html?: string): string => {\n if (!html) return ''\n\n try {\n return DOMPurify.sanitize(html, { ALLOWED_TAGS: [], KEEP_CONTENT: true })\n } catch {\n return html.replace(/<\\/?[^>]+(>|$)/g, '')\n }\n}\n","import { stripHtmlTags } from './stripHtmlTags'\n\n/**\n * Normalizes HTML into a lowercase, punctuation-free, single-spaced token string\n * for search indexing/matching. From alerts' cleanHTML; builds on stripHtmlTags\n * so tag removal stays single-sourced.\n * @param {string} html - The HTML string to clean.\n * @returns {string} The cleaned, normalized text.\n */\nexport const cleanHTML = (html: string): string =>\n stripHtmlTags(html)\n .toLowerCase()\n .replace(/[.,!?;:\"'()[\\]\\-_/\\\\]/g, ' ')\n .replace(/\\s+/g, ' ')\n .trim()\n","import type { Config } from 'dompurify'\n\n/**\n * DOMPurify custom-element handling that preserves embedded-widget custom\n * elements (any hyphenated tag, e.g. `<news-teaser>`) and their kebab-case /\n * `data-` attributes so the host widget manager (renderWidgets) can hydrate\n * them. Without this, DOMPurify's default profile silently strips unknown custom\n * elements AND their attributes, wiping embeds out of the content before they can\n * render. `on*` event-handler attributes are still rejected (defense in depth\n * alongside DOMPurify's own XSS stripping). Promoted from staffbase-global-content.\n */\nexport const customElementHandling: NonNullable<\n Config['CUSTOM_ELEMENT_HANDLING']\n> = {\n // Any valid custom-element tag name (must contain a hyphen per the spec).\n tagNameCheck: /^[a-z][a-z0-9]*-[a-z0-9-]*$/,\n // Allow kebab-case / data- attributes on custom elements, but never `on*`.\n attributeNameCheck: /^(?!on)[a-z][a-z0-9]*([-:][a-z0-9]+)*$/,\n // Permit `<div is=\"some-widget\">`-style customized built-in elements.\n allowCustomizedBuiltInElements: true,\n}\n","/**\n * Warns (once per call) when the sanitizer dropped what looks like an embedded\n * widget — a custom-element tag (any hyphenated tag name). These removals are the\n * silent failure mode that makes embedded widgets \"disappear\" before the host can\n * render them, so surfacing them aids diagnosis. Scripts, styles and on*\n * handlers are intentionally removed and are NOT reported (they would be noise).\n * @param {readonly unknown[]} removed - DOMPurify's `removed` array after sanitize.\n * @returns {void}\n */\nexport const logRemovedEmbeds = (removed: readonly unknown[]): void => {\n const tags: string[] = []\n\n for (const entry of removed) {\n if (!entry || typeof entry !== 'object' || !('element' in entry)) continue\n const element = (entry as { element: unknown }).element\n if (element instanceof Element) {\n const tag = element.tagName.toLowerCase()\n if (tag.includes('-')) tags.push(tag)\n }\n }\n\n if (tags.length > 0) {\n console.warn(\n `[staffbase-utils] sanitizer removed ${tags.length} possible widget embed(s): ${tags.join(', ')} — they will not render. If these are valid embeds, the sanitizer config may need updating.`,\n )\n }\n}\n","import createDOMPurify from 'dompurify'\n\nimport type { SanitizeArticleHtmlOptions } from '../types/html/SanitizeArticleHtmlOptions'\nimport { customElementHandling } from './customElementHandling'\nimport { logRemovedEmbeds } from './logRemovedEmbeds'\n\n// Dedicated DOMPurify instance so the hardening hook below is scoped to this\n// sanitizer and never pollutes the consumer's shared default DOMPurify instance\n// (other code may sanitize through the default instance directly).\nconst purifier = createDOMPurify(window)\n\n// Set synchronously around each (synchronous) sanitize call. JS is\n// single-threaded, so the guard cannot leak across calls.\nlet activeIframeGuard: ((src: string) => boolean) | null = null\n\n// afterSanitizeAttributes hook: force rel=\"noopener noreferrer\" on\n// target=\"_blank\" anchors (reverse-tabnabbing), and drop iframes whose src fails\n// the injected allowlist predicate when one is active.\npurifier.addHook('afterSanitizeAttributes', (node) => {\n if (!(node instanceof Element)) return\n\n if (node.tagName === 'A' && node.getAttribute('target') === '_blank') {\n node.setAttribute('rel', 'noopener noreferrer')\n }\n\n if (node.tagName === 'IFRAME' && activeIframeGuard) {\n const src = node.getAttribute('src') ?? ''\n if (!activeIframeGuard(src)) node.parentNode?.removeChild(node)\n }\n})\n\n/**\n * Canonical sanitizer for rich article HTML rendered into a session-bearing\n * webview. Superset of the alerts and unacknowledged-bulletins variants: keeps\n * iframes and data-* attributes (renderWidgets discovers embedded sub-widgets via\n * data-*), strips scripts/inline handlers/javascript:, and forces\n * rel=\"noopener noreferrer\" on target=\"_blank\" anchors. When options.isAllowedIframeSrc\n * is provided, iframes whose src fails it are dropped (injected so this module\n * does not depend on /links, which owns isAllowedIframeSrc).\n * @param {string} html - Raw article HTML from the Staffbase API.\n * @param {SanitizeArticleHtmlOptions} options - Optional iframe-src allowlist.\n * @returns {string} Sanitized HTML safe to inject/parse.\n */\nexport const sanitizeArticleHtml = (\n html: string,\n options: SanitizeArticleHtmlOptions = {},\n): string => {\n activeIframeGuard = options.isAllowedIframeSrc ?? null\n try {\n const clean = purifier.sanitize(html, {\n USE_PROFILES: { html: true },\n CUSTOM_ELEMENT_HANDLING: customElementHandling,\n ADD_TAGS: ['iframe'],\n ADD_ATTR: [\n 'target',\n 'allow',\n 'allowfullscreen',\n 'frameborder',\n 'scrolling',\n 'loading',\n 'referrerpolicy',\n ],\n FORBID_TAGS: ['script', 'style'],\n FORBID_ATTR: ['onerror', 'onload', 'onclick'],\n })\n logRemovedEmbeds(purifier.removed)\n return clean\n } finally {\n activeIframeGuard = null\n }\n}\n","import DOMPurify from 'dompurify'\n\nimport { customElementHandling } from './customElementHandling'\nimport { logRemovedEmbeds } from './logRemovedEmbeds'\n\n/**\n * Strict sanitizer for untrusted snippet/teaser HTML rendered through a\n * non-sanitizing parser (e.g. html-react-parser). The default DOMPurify profile\n * strips scripts, inline handlers and dangerous URLs AND drops iframes, keeping\n * only basic rich-text markup. Embedded-widget custom elements (and their\n * kebab/data attributes) are preserved so the host can hydrate them. From\n * global-content's sanitizeHtml.\n *\n * Use sanitizeArticleHtml instead when rendering full article bodies that must\n * keep iframes / data-* embeds.\n * @param {string} html - Raw HTML string from the API.\n * @returns {string} Sanitized HTML.\n */\nexport const sanitizeHtml = (html: string): string => {\n const clean = DOMPurify.sanitize(html, {\n CUSTOM_ELEMENT_HANDLING: customElementHandling,\n })\n logRemovedEmbeds(DOMPurify.removed)\n return clean\n}\n"],"mappings":"8IAUA,IAAa,EAAiB,GAA0B,CACtD,GAAI,CAAC,EAAM,MAAO,GAElB,GAAI,CACF,OAAO,EAAA,QAAU,SAAS,EAAM,CAAE,aAAc,CAAC,EAAG,aAAc,EAAK,CAAC,CAC1E,MAAQ,CACN,OAAO,EAAK,QAAQ,kBAAmB,EAAE,CAC3C,CACF,ECTa,EAAa,GACxB,EAAc,CAAI,EACf,YAAY,EACZ,QAAQ,yBAA0B,GAAG,EACrC,QAAQ,OAAQ,GAAG,EACnB,KAAK,ECHG,EAET,CAEF,aAAc,8BAEd,mBAAoB,yCAEpB,+BAAgC,EAClC,ECXa,EAAoB,GAAsC,CACrE,IAAM,EAAiB,CAAC,EAExB,IAAK,IAAM,KAAS,EAAS,CAC3B,GAAI,CAAC,GAAS,OAAO,GAAU,UAAY,EAAE,YAAa,GAAQ,SAClE,IAAM,EAAW,EAA+B,QAChD,GAAI,aAAmB,QAAS,CAC9B,IAAM,EAAM,EAAQ,QAAQ,YAAY,EACpC,EAAI,SAAS,GAAG,GAAG,EAAK,KAAK,CAAG,CACtC,CACF,CAEI,EAAK,OAAS,GAChB,QAAQ,KACN,uCAAuC,EAAK,OAAO,6BAA6B,EAAK,KAAK,IAAI,EAAE,4FAClG,CAEJ,ECjBM,GAAA,EAAA,EAAA,SAA2B,MAAM,EAInC,EAAuD,KAK3D,EAAS,QAAQ,0BAA4B,GAAS,CAC9C,gBAAgB,UAElB,EAAK,UAAY,KAAO,EAAK,aAAa,QAAQ,IAAM,UAC1D,EAAK,aAAa,MAAO,qBAAqB,EAG5C,EAAK,UAAY,UAAY,GAAmB,CAClD,IAAM,EAAM,EAAK,aAAa,KAAK,GAAK,GACnC,EAAkB,CAAG,GAAG,EAAK,YAAY,YAAY,CAAI,CAChE,CACF,CAAC,EAcD,IAAa,GACX,EACA,EAAsC,CAAC,IAC5B,CACX,EAAoB,EAAQ,oBAAsB,KAClD,GAAI,CACF,IAAM,EAAQ,EAAS,SAAS,EAAM,CACpC,aAAc,CAAE,KAAM,EAAK,EAC3B,wBAAyB,EACzB,SAAU,CAAC,QAAQ,EACnB,SAAU,CACR,SACA,QACA,kBACA,cACA,YACA,UACA,gBACF,EACA,YAAa,CAAC,SAAU,OAAO,EAC/B,YAAa,CAAC,UAAW,SAAU,SAAS,CAC9C,CAAC,EAED,OADA,EAAiB,EAAS,OAAO,EAC1B,CACT,QAAU,CACR,EAAoB,IACtB,CACF,ECpDa,EAAgB,GAAyB,CACpD,IAAM,EAAQ,EAAA,QAAU,SAAS,EAAM,CACrC,wBAAyB,CAC3B,CAAC,EAED,OADA,EAAiB,EAAA,QAAU,OAAO,EAC3B,CACT"}
@@ -0,0 +1,15 @@
1
+ //#region src/shadow/isShadowRoot.ts
2
+ var e = (e) => !e || typeof e != "object" ? !1 : e.nodeType === Node.DOCUMENT_FRAGMENT_NODE && "host" in e && "mode" in e, t = (t, n, r) => {
3
+ if (!e(t)) throw Error("injectShadowStyles: shadowRoot must be a ShadowRoot instance. CSS must never be injected outside shadowRoot for style isolation.");
4
+ let i = t.getElementById(r);
5
+ if (i instanceof HTMLStyleElement) {
6
+ i.textContent !== n && (i.textContent = n);
7
+ return;
8
+ }
9
+ let a = document.createElement("style");
10
+ a.id = r, a.textContent = n, t.appendChild(a);
11
+ };
12
+ //#endregion
13
+ export { e as n, t };
14
+
15
+ //# sourceMappingURL=injectShadowStyles-C40qdwKI.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"injectShadowStyles-C40qdwKI.mjs","names":[],"sources":["../src/shadow/isShadowRoot.ts","../src/shadow/injectShadowStyles.ts"],"sourcesContent":["/**\n * Type guard for a real ShadowRoot, used to ensure style injection never targets\n * `document.head`/`document.body` (which would break Shadow DOM style isolation).\n * @param {unknown} value - The candidate value.\n * @returns {boolean} True when the value is a ShadowRoot.\n */\nexport const isShadowRoot = (value: unknown): value is ShadowRoot => {\n if (!value || typeof value !== 'object') return false\n\n const nodeType = (value as Node).nodeType\n return (\n nodeType === Node.DOCUMENT_FRAGMENT_NODE &&\n 'host' in value &&\n 'mode' in value\n )\n}\n","import { isShadowRoot } from './isShadowRoot'\n\n/**\n * Injects a CSS string into a ShadowRoot as a `<style>` element.\n *\n * Idempotent: reuses the element with the given id and only updates it when the\n * CSS changed. Throws when the target is not a ShadowRoot, so styles can never\n * leak outside the shadow tree.\n * @param {ShadowRoot} shadowRoot - The shadow root to inject into (required for isolation).\n * @param {string} cssText - The CSS to inject.\n * @param {string} styleElementId - Stable id for the injected `<style>` element.\n * @returns {void} Nothing.\n * @throws {Error} When `shadowRoot` is not a ShadowRoot instance.\n */\nexport const injectShadowStyles = (\n shadowRoot: ShadowRoot,\n cssText: string,\n styleElementId: string,\n): void => {\n if (!isShadowRoot(shadowRoot)) {\n throw new Error(\n 'injectShadowStyles: shadowRoot must be a ShadowRoot instance. ' +\n 'CSS must never be injected outside shadowRoot for style isolation.',\n )\n }\n\n const existing = shadowRoot.getElementById(styleElementId)\n if (existing instanceof HTMLStyleElement) {\n if (existing.textContent !== cssText) {\n existing.textContent = cssText\n }\n return\n }\n\n const styleEl = document.createElement('style')\n styleEl.id = styleElementId\n styleEl.textContent = cssText\n shadowRoot.appendChild(styleEl)\n}\n"],"mappings":";AAMA,IAAa,KAAgB,MACvB,CAAC,KAAS,OAAO,KAAU,WAAiB,KAE9B,EAAe,aAElB,KAAK,0BAClB,UAAU,KACV,UAAU,GCCD,KACX,GACA,GACA,MACS;CACT,IAAI,CAAC,EAAa,CAAU,GAC1B,MAAU,MACR,kIAEF;CAGF,IAAM,IAAW,EAAW,eAAe,CAAc;CACzD,IAAI,aAAoB,kBAAkB;EACxC,AAAI,EAAS,gBAAgB,MAC3B,EAAS,cAAc;EAEzB;CACF;CAEA,IAAM,IAAU,SAAS,cAAc,OAAO;CAG9C,AAFA,EAAQ,KAAK,GACb,EAAQ,cAAc,GACtB,EAAW,YAAY,CAAO;AAChC"}
@@ -0,0 +1,2 @@
1
+ var e=e=>!e||typeof e!=`object`?!1:e.nodeType===Node.DOCUMENT_FRAGMENT_NODE&&`host`in e&&`mode`in e,t=(t,n,r)=>{if(!e(t))throw Error(`injectShadowStyles: shadowRoot must be a ShadowRoot instance. CSS must never be injected outside shadowRoot for style isolation.`);let i=t.getElementById(r);if(i instanceof HTMLStyleElement){i.textContent!==n&&(i.textContent=n);return}let a=document.createElement(`style`);a.id=r,a.textContent=n,t.appendChild(a)};Object.defineProperty(exports,"n",{enumerable:!0,get:function(){return e}}),Object.defineProperty(exports,"t",{enumerable:!0,get:function(){return t}});
2
+ //# sourceMappingURL=injectShadowStyles-SjxVBpXH.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"injectShadowStyles-SjxVBpXH.js","names":[],"sources":["../src/shadow/isShadowRoot.ts","../src/shadow/injectShadowStyles.ts"],"sourcesContent":["/**\n * Type guard for a real ShadowRoot, used to ensure style injection never targets\n * `document.head`/`document.body` (which would break Shadow DOM style isolation).\n * @param {unknown} value - The candidate value.\n * @returns {boolean} True when the value is a ShadowRoot.\n */\nexport const isShadowRoot = (value: unknown): value is ShadowRoot => {\n if (!value || typeof value !== 'object') return false\n\n const nodeType = (value as Node).nodeType\n return (\n nodeType === Node.DOCUMENT_FRAGMENT_NODE &&\n 'host' in value &&\n 'mode' in value\n )\n}\n","import { isShadowRoot } from './isShadowRoot'\n\n/**\n * Injects a CSS string into a ShadowRoot as a `<style>` element.\n *\n * Idempotent: reuses the element with the given id and only updates it when the\n * CSS changed. Throws when the target is not a ShadowRoot, so styles can never\n * leak outside the shadow tree.\n * @param {ShadowRoot} shadowRoot - The shadow root to inject into (required for isolation).\n * @param {string} cssText - The CSS to inject.\n * @param {string} styleElementId - Stable id for the injected `<style>` element.\n * @returns {void} Nothing.\n * @throws {Error} When `shadowRoot` is not a ShadowRoot instance.\n */\nexport const injectShadowStyles = (\n shadowRoot: ShadowRoot,\n cssText: string,\n styleElementId: string,\n): void => {\n if (!isShadowRoot(shadowRoot)) {\n throw new Error(\n 'injectShadowStyles: shadowRoot must be a ShadowRoot instance. ' +\n 'CSS must never be injected outside shadowRoot for style isolation.',\n )\n }\n\n const existing = shadowRoot.getElementById(styleElementId)\n if (existing instanceof HTMLStyleElement) {\n if (existing.textContent !== cssText) {\n existing.textContent = cssText\n }\n return\n }\n\n const styleEl = document.createElement('style')\n styleEl.id = styleElementId\n styleEl.textContent = cssText\n shadowRoot.appendChild(styleEl)\n}\n"],"mappings":"AAMA,IAAa,EAAgB,GACvB,CAAC,GAAS,OAAO,GAAU,SAAiB,GAE9B,EAAe,WAElB,KAAK,wBAClB,SAAU,GACV,SAAU,ECCD,GACX,EACA,EACA,IACS,CACT,GAAI,CAAC,EAAa,CAAU,EAC1B,MAAU,MACR,kIAEF,EAGF,IAAM,EAAW,EAAW,eAAe,CAAc,EACzD,GAAI,aAAoB,iBAAkB,CACpC,EAAS,cAAgB,IAC3B,EAAS,YAAc,GAEzB,MACF,CAEA,IAAM,EAAU,SAAS,cAAc,OAAO,EAC9C,EAAQ,GAAK,EACb,EAAQ,YAAc,EACtB,EAAW,YAAY,CAAO,CAChC"}
@@ -1 +1 @@
1
- {"version":3,"file":"react.cjs.js","names":[],"sources":["../../src/links/react/hasMouseEventProperties.ts","../../src/links/react/isModifiedNativeEvent.ts","../../src/links/react/isNativeLinkEvent.ts","../../src/links/react/isTouchLinkEvent.ts","../../src/links/react/nativeLinkHandlers.ts","../../src/links/react/tryStaffbaseContentOpenLink.ts","../../src/links/react/useInAppLinkHandling.ts"],"sourcesContent":["import type { NativeLinkEvent } from '../../types/links/NativeLinkEvent'\n\n/**\n * Whether a native link event exposes MouseEvent modifier properties.\n * @param {NativeLinkEvent} e - The native event.\n * @returns {boolean} True when the event has `button` and `metaKey`.\n */\nexport const hasMouseEventProperties = (\n e: NativeLinkEvent,\n): e is MouseEvent | PointerEvent => {\n return 'button' in e && 'metaKey' in e\n}\n","import type { NativeLinkEvent } from '../../types/links/NativeLinkEvent'\nimport { hasMouseEventProperties } from './hasMouseEventProperties'\n\n/**\n * Whether a native link event is \"modified\" and should be left to the browser\n * (default prevented, a non-primary mouse button, or a modifier key held).\n * @param {NativeLinkEvent} e - The native event.\n * @returns {boolean} True when the event should not be intercepted.\n */\nexport const isModifiedNativeEvent = (e: NativeLinkEvent): boolean => {\n const hasMouseProps = hasMouseEventProperties(e)\n const button = hasMouseProps && 'button' in e ? e.button : undefined\n const metaKey = hasMouseProps ? e.metaKey : false\n const ctrlKey = hasMouseProps ? e.ctrlKey : false\n const shiftKey = hasMouseProps ? e.shiftKey : false\n const altKey = hasMouseProps ? e.altKey : false\n\n return Boolean(\n e.defaultPrevented ||\n (typeof button === 'number' && button !== 0) ||\n metaKey ||\n ctrlKey ||\n shiftKey ||\n altKey,\n )\n}\n","import type { NativeLinkEvent } from '../../types/links/NativeLinkEvent'\n\n/**\n * Whether a value is a real native DOM event (as opposed to a React synthetic\n * event-like object), so the Staffbase content open-link path can be tried.\n * @param {NativeLinkEvent | { preventDefault?: () => void; stopPropagation?: () => void } | undefined} e - The candidate.\n * @returns {boolean} True when the value is a native Event.\n */\nexport const isNativeLinkEvent = (\n e:\n | { preventDefault?: () => void; stopPropagation?: () => void }\n | NativeLinkEvent\n | undefined,\n): e is NativeLinkEvent => {\n return e !== undefined && 'target' in e && e instanceof Event\n}\n","import type { NativeLinkEvent } from '../../types/links/NativeLinkEvent'\n\n/**\n * Whether a native link event is a TouchEvent (has `changedTouches`).\n * @param {NativeLinkEvent} e - The native event.\n * @returns {boolean} True when the event is a TouchEvent.\n */\nexport const isTouchLinkEvent = (e: NativeLinkEvent): e is TouchEvent => {\n return 'changedTouches' in e\n}\n","import type { NativeLinkEvent } from '../../types/links/NativeLinkEvent'\n\n/**\n * Tracks the capture-phase handler installed per container element, so the same\n * container is never wired twice and can be cleanly torn down on unmount.\n */\nexport const nativeLinkHandlers = new WeakMap<\n HTMLElement,\n (e: NativeLinkEvent) => void\n>()\n","import type { NativeLinkEvent } from '../../types/links/NativeLinkEvent'\nimport type { StaffbaseContentWindow } from '../../types/links/StaffbaseContentWindow'\n\n/**\n * Tries the host's own content open-link helper (the one Staffbase's widget\n * manager uses), to stay maximally aligned with platform behavior. Returns false\n * when the helper is absent or throws, so the caller can fall back.\n * @param {NativeLinkEvent} e - The originating native event.\n * @returns {boolean} True when the host helper handled the open.\n */\nexport const tryStaffbaseContentOpenLink = (e: NativeLinkEvent): boolean => {\n const staffbase = (window as unknown as StaffbaseContentWindow).staffbase\n const fn =\n staffbase?.content?.link?.openLink ||\n staffbase?.content?.links?.openLink ||\n staffbase?.content?.loader?.link?.openLink\n\n if (typeof fn !== 'function') return false\n\n try {\n fn({ useDefault: false }, e)\n return true\n } catch {\n // The host helper is best-effort; fall back to standard navigation on error.\n return false\n }\n}\n","import type {\n MouseEvent as ReactMouseEvent,\n PointerEvent as ReactPointerEvent,\n TouchEvent as ReactTouchEvent,\n} from 'react'\nimport { useCallback, useEffect, useRef } from 'react'\n\nimport type { NativeLinkEvent } from '../../types/links/NativeLinkEvent'\nimport type { UseInAppLinkHandlingOptions } from '../../types/links/UseInAppLinkHandlingOptions'\nimport type { UseInAppLinkHandlingReturn } from '../../types/links/UseInAppLinkHandlingReturn'\nimport { getInAppOpenLinkTarget } from '../getInAppOpenLinkTarget'\nimport { normalizeInAppLinks } from '../normalizeInAppLinks'\nimport { openStaffbaseAware } from '../openStaffbaseAware'\nimport { isModifiedNativeEvent } from './isModifiedNativeEvent'\nimport { isNativeLinkEvent } from './isNativeLinkEvent'\nimport { isTouchLinkEvent } from './isTouchLinkEvent'\nimport { nativeLinkHandlers } from './nativeLinkHandlers'\nimport { tryStaffbaseContentOpenLink } from './tryStaffbaseContentOpenLink'\n\nconst SUPPRESS_CLICK_WINDOW_MS = 700\n\n/**\n * Centralizes in-content link handling for Staffbase mobile/webview environments.\n *\n * Always attempts Staffbase-aware navigation (the host openLink when available,\n * otherwise same-tab navigation). It exposes React handlers for click/touchend/\n * pointerup and, when `containerRef` is given, installs a more reliable native\n * capture-phase handler. Touch/pointer/click are de-duplicated within a short\n * window so a single tap never fires navigation twice in iOS webviews.\n * @param {UseInAppLinkHandlingOptions} options - Origin, after-open callback and optional container ref.\n * @returns {UseInAppLinkHandlingReturn} prepareHtmlContent plus the React event handlers.\n */\nexport const useInAppLinkHandling = ({\n staffbaseOrigin,\n onAfterOpen,\n containerRef,\n}: UseInAppLinkHandlingOptions): UseInAppLinkHandlingReturn => {\n const lastTouchAtRef = useRef<number | null>(null)\n const lastHandledAtRef = useRef<number | null>(null)\n const lastHandledTypeRef = useRef<'touch' | 'pointer' | null>(null)\n const lastHandledAnchorRef = useRef<HTMLAnchorElement | null>(null)\n\n const prepareHtmlContent = useCallback(\n (content: HTMLElement): HTMLElement => {\n normalizeInAppLinks(content, staffbaseOrigin)\n return content\n },\n [staffbaseOrigin],\n )\n\n const handleAnchor = useCallback(\n (\n anchor: HTMLAnchorElement,\n e?:\n | { preventDefault?: () => void; stopPropagation?: () => void }\n | NativeLinkEvent,\n ) => {\n const linkToOpen = getInAppOpenLinkTarget(\n anchor.getAttribute('href') ?? anchor.href ?? null,\n staffbaseOrigin,\n )\n if (!linkToOpen) return\n\n // Handle both React synthetic event-like objects and native events.\n if (e && 'preventDefault' in e) {\n e.preventDefault?.()\n e.stopPropagation?.()\n }\n\n // Prefer Staffbase's own content link handler when this is a native event.\n if (isNativeLinkEvent(e)) {\n // Touch events in some webviews don't trigger Staffbase openLink reliably.\n if (isTouchLinkEvent(e)) {\n openStaffbaseAware(linkToOpen)\n onAfterOpen?.()\n return\n }\n if (!tryStaffbaseContentOpenLink(e)) {\n openStaffbaseAware(linkToOpen)\n }\n } else {\n openStaffbaseAware(linkToOpen)\n }\n onAfterOpen?.()\n },\n [onAfterOpen, staffbaseOrigin],\n )\n\n const handleContentClick = useCallback(\n (e: ReactMouseEvent<HTMLElement>) => {\n if (e.defaultPrevented) return\n if (\n lastTouchAtRef.current &&\n Date.now() - lastTouchAtRef.current < SUPPRESS_CLICK_WINDOW_MS\n ) {\n return\n }\n\n const target = e.target\n if (!(target instanceof Element)) return\n\n const anchor = target.closest('a')\n if (!anchor) return\n handleAnchor(anchor, e)\n },\n [handleAnchor],\n )\n\n const handleContentTouchEnd = useCallback(\n (e: ReactTouchEvent<HTMLElement>) => {\n if (e.defaultPrevented) return\n const target = e.target\n if (!(target instanceof Element)) return\n\n const anchor = target.closest('a')\n if (!anchor) return\n\n if (\n lastHandledAtRef.current &&\n lastHandledTypeRef.current === 'pointer' &&\n lastHandledAnchorRef.current === anchor &&\n Date.now() - lastHandledAtRef.current < SUPPRESS_CLICK_WINDOW_MS\n ) {\n return\n }\n\n lastTouchAtRef.current = Date.now()\n lastHandledAtRef.current = lastTouchAtRef.current\n lastHandledTypeRef.current = 'touch'\n lastHandledAnchorRef.current = anchor\n handleAnchor(anchor, e.nativeEvent)\n },\n [handleAnchor],\n )\n\n const handleContentPointerUp = useCallback(\n (e: ReactPointerEvent<HTMLElement>) => {\n if (e.defaultPrevented) return\n const target = e.target\n if (!(target instanceof Element)) return\n\n const anchor = target.closest('a')\n if (!anchor) return\n\n if (\n lastHandledAtRef.current &&\n lastHandledTypeRef.current === 'touch' &&\n lastHandledAnchorRef.current === anchor &&\n Date.now() - lastHandledAtRef.current < SUPPRESS_CLICK_WINDOW_MS\n ) {\n return\n }\n\n lastTouchAtRef.current = Date.now()\n lastHandledAtRef.current = lastTouchAtRef.current\n lastHandledTypeRef.current = 'pointer'\n lastHandledAnchorRef.current = anchor\n handleAnchor(anchor, e.nativeEvent)\n },\n [handleAnchor],\n )\n\n useEffect(() => {\n const container = containerRef?.current\n if (!container) return\n\n if (nativeLinkHandlers.has(container)) return\n\n /**\n * Capture-phase handler intercepting link activations on the container.\n * @param {NativeLinkEvent} e - The native click/touchend/pointerup event.\n * @returns {void} Nothing.\n */\n const handler = (e: NativeLinkEvent) => {\n try {\n if (isModifiedNativeEvent(e)) return\n const target = e.target\n if (!(target instanceof Element)) return\n\n const anchor = target.closest('a')\n if (!anchor) return\n\n const now = Date.now()\n if (\n lastHandledAtRef.current &&\n lastHandledAnchorRef.current === anchor &&\n now - lastHandledAtRef.current < SUPPRESS_CLICK_WINDOW_MS\n ) {\n return\n }\n\n lastHandledAtRef.current = now\n lastHandledTypeRef.current = isTouchLinkEvent(e) ? 'touch' : 'pointer'\n lastHandledAnchorRef.current = anchor\n handleAnchor(anchor, e)\n } catch {\n // Capture-phase handler must never throw into the host event loop.\n }\n }\n\n nativeLinkHandlers.set(container, handler)\n container.addEventListener('click', handler, true)\n container.addEventListener('touchend', handler, true)\n // Pointer events may be enabled in some webviews; harmless if unused.\n container.addEventListener('pointerup', handler, true)\n\n return () => {\n container.removeEventListener('click', handler, true)\n container.removeEventListener('touchend', handler, true)\n container.removeEventListener('pointerup', handler, true)\n nativeLinkHandlers.delete(container)\n }\n }, [containerRef, handleAnchor])\n\n return {\n prepareHtmlContent,\n handleContentClick,\n handleContentTouchEnd,\n handleContentPointerUp,\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"react.cjs.js","names":[],"sources":["../../src/links/react/hasMouseEventProperties.ts","../../src/links/react/isModifiedNativeEvent.ts","../../src/links/react/isNativeLinkEvent.ts","../../src/links/react/isTouchLinkEvent.ts","../../src/links/react/nativeLinkHandlers.ts","../../src/links/react/tryStaffbaseContentOpenLink.ts","../../src/links/react/useInAppLinkHandling.ts"],"sourcesContent":["import type { NativeLinkEvent } from '../../types/links/NativeLinkEvent'\n\n/**\n * Whether a native link event exposes MouseEvent modifier properties.\n * @param {NativeLinkEvent} e - The native event.\n * @returns {boolean} True when the event has `button` and `metaKey`.\n */\nexport const hasMouseEventProperties = (\n e: NativeLinkEvent,\n): e is MouseEvent | PointerEvent => {\n return 'button' in e && 'metaKey' in e\n}\n","import type { NativeLinkEvent } from '../../types/links/NativeLinkEvent'\nimport { hasMouseEventProperties } from './hasMouseEventProperties'\n\n/**\n * Whether a native link event is \"modified\" and should be left to the browser\n * (default prevented, a non-primary mouse button, or a modifier key held).\n * @param {NativeLinkEvent} e - The native event.\n * @returns {boolean} True when the event should not be intercepted.\n */\nexport const isModifiedNativeEvent = (e: NativeLinkEvent): boolean => {\n const hasMouseProps = hasMouseEventProperties(e)\n const button = hasMouseProps && 'button' in e ? e.button : undefined\n const metaKey = hasMouseProps ? e.metaKey : false\n const ctrlKey = hasMouseProps ? e.ctrlKey : false\n const shiftKey = hasMouseProps ? e.shiftKey : false\n const altKey = hasMouseProps ? e.altKey : false\n\n return Boolean(\n e.defaultPrevented ||\n (typeof button === 'number' && button !== 0) ||\n metaKey ||\n ctrlKey ||\n shiftKey ||\n altKey,\n )\n}\n","import type { NativeLinkEvent } from '../../types/links/NativeLinkEvent'\n\n/**\n * Whether a value is a real native DOM event (as opposed to a React synthetic\n * event-like object), so the Staffbase content open-link path can be tried.\n * @param {NativeLinkEvent | { preventDefault?: () => void; stopPropagation?: () => void } | undefined} e - The candidate.\n * @returns {boolean} True when the value is a native Event.\n */\nexport const isNativeLinkEvent = (\n e:\n | { preventDefault?: () => void; stopPropagation?: () => void }\n | NativeLinkEvent\n | undefined,\n): e is NativeLinkEvent => {\n return e !== undefined && 'target' in e && e instanceof Event\n}\n","import type { NativeLinkEvent } from '../../types/links/NativeLinkEvent'\n\n/**\n * Whether a native link event is a TouchEvent (has `changedTouches`).\n * @param {NativeLinkEvent} e - The native event.\n * @returns {boolean} True when the event is a TouchEvent.\n */\nexport const isTouchLinkEvent = (e: NativeLinkEvent): e is TouchEvent => {\n return 'changedTouches' in e\n}\n","import type { NativeLinkEvent } from '../../types/links/NativeLinkEvent'\n\n/**\n * Tracks the capture-phase handler installed per container element, so the same\n * container is never wired twice and can be cleanly torn down on unmount.\n */\nexport const nativeLinkHandlers = new WeakMap<\n HTMLElement,\n (e: NativeLinkEvent) => void\n>()\n","import type { NativeLinkEvent } from '../../types/links/NativeLinkEvent'\nimport type { StaffbaseContentWindow } from '../../types/links/StaffbaseContentWindow'\n\n/**\n * Tries the host's own content open-link helper (the one Staffbase's widget\n * manager uses), to stay maximally aligned with platform behavior. Returns false\n * when the helper is absent or throws, so the caller can fall back.\n * @param {NativeLinkEvent} e - The originating native event.\n * @returns {boolean} True when the host helper handled the open.\n */\nexport const tryStaffbaseContentOpenLink = (e: NativeLinkEvent): boolean => {\n const staffbase = (window as unknown as StaffbaseContentWindow).staffbase\n const fn =\n staffbase?.content?.link?.openLink ||\n staffbase?.content?.links?.openLink ||\n staffbase?.content?.loader?.link?.openLink\n\n if (typeof fn !== 'function') return false\n\n try {\n fn({ useDefault: false }, e)\n return true\n } catch {\n // The host helper is best-effort; fall back to standard navigation on error.\n return false\n }\n}\n","import type {\n MouseEvent as ReactMouseEvent,\n PointerEvent as ReactPointerEvent,\n TouchEvent as ReactTouchEvent,\n} from 'react'\nimport { useCallback, useEffect, useRef } from 'react'\n\nimport type { NativeLinkEvent } from '../../types/links/NativeLinkEvent'\nimport type { UseInAppLinkHandlingOptions } from '../../types/links/UseInAppLinkHandlingOptions'\nimport type { UseInAppLinkHandlingReturn } from '../../types/links/UseInAppLinkHandlingReturn'\nimport { getInAppOpenLinkTarget } from '../getInAppOpenLinkTarget'\nimport { normalizeInAppLinks } from '../normalizeInAppLinks'\nimport { openStaffbaseAware } from '../openStaffbaseAware'\nimport { isModifiedNativeEvent } from './isModifiedNativeEvent'\nimport { isNativeLinkEvent } from './isNativeLinkEvent'\nimport { isTouchLinkEvent } from './isTouchLinkEvent'\nimport { nativeLinkHandlers } from './nativeLinkHandlers'\nimport { tryStaffbaseContentOpenLink } from './tryStaffbaseContentOpenLink'\n\nconst SUPPRESS_CLICK_WINDOW_MS = 700\n\n/**\n * Centralizes in-content link handling for Staffbase mobile/webview environments.\n *\n * Always attempts Staffbase-aware navigation (the host openLink when available,\n * otherwise same-tab navigation). It exposes React handlers for click/touchend/\n * pointerup and, when `containerRef` is given, installs a more reliable native\n * capture-phase handler. Touch/pointer/click are de-duplicated within a short\n * window so a single tap never fires navigation twice in iOS webviews.\n * @param {UseInAppLinkHandlingOptions} options - Origin, after-open callback and optional container ref.\n * @returns {UseInAppLinkHandlingReturn} prepareHtmlContent plus the React event handlers.\n */\nexport const useInAppLinkHandling = ({\n staffbaseOrigin,\n onAfterOpen,\n containerRef,\n}: UseInAppLinkHandlingOptions): UseInAppLinkHandlingReturn => {\n const lastTouchAtRef = useRef<number | null>(null)\n const lastHandledAtRef = useRef<number | null>(null)\n const lastHandledTypeRef = useRef<'touch' | 'pointer' | null>(null)\n const lastHandledAnchorRef = useRef<HTMLAnchorElement | null>(null)\n\n const prepareHtmlContent = useCallback(\n (content: HTMLElement): HTMLElement => {\n normalizeInAppLinks(content, staffbaseOrigin)\n return content\n },\n [staffbaseOrigin],\n )\n\n const handleAnchor = useCallback(\n (\n anchor: HTMLAnchorElement,\n e?:\n | { preventDefault?: () => void; stopPropagation?: () => void }\n | NativeLinkEvent,\n ) => {\n const linkToOpen = getInAppOpenLinkTarget(\n anchor.getAttribute('href') ?? anchor.href ?? null,\n staffbaseOrigin,\n )\n if (!linkToOpen) return\n\n // Handle both React synthetic event-like objects and native events.\n if (e && 'preventDefault' in e) {\n e.preventDefault?.()\n e.stopPropagation?.()\n }\n\n // Prefer Staffbase's own content link handler when this is a native event.\n if (isNativeLinkEvent(e)) {\n // Touch events in some webviews don't trigger Staffbase openLink reliably.\n if (isTouchLinkEvent(e)) {\n openStaffbaseAware(linkToOpen)\n onAfterOpen?.()\n return\n }\n if (!tryStaffbaseContentOpenLink(e)) {\n openStaffbaseAware(linkToOpen)\n }\n } else {\n openStaffbaseAware(linkToOpen)\n }\n onAfterOpen?.()\n },\n [onAfterOpen, staffbaseOrigin],\n )\n\n const handleContentClick = useCallback(\n (e: ReactMouseEvent<HTMLElement>) => {\n if (e.defaultPrevented) return\n if (\n lastTouchAtRef.current &&\n Date.now() - lastTouchAtRef.current < SUPPRESS_CLICK_WINDOW_MS\n ) {\n return\n }\n\n const target = e.target\n if (!(target instanceof Element)) return\n\n const anchor = target.closest('a')\n if (!anchor) return\n handleAnchor(anchor, e)\n },\n [handleAnchor],\n )\n\n const handleContentTouchEnd = useCallback(\n (e: ReactTouchEvent<HTMLElement>) => {\n if (e.defaultPrevented) return\n const target = e.target\n if (!(target instanceof Element)) return\n\n const anchor = target.closest('a')\n if (!anchor) return\n\n if (\n lastHandledAtRef.current &&\n lastHandledTypeRef.current === 'pointer' &&\n lastHandledAnchorRef.current === anchor &&\n Date.now() - lastHandledAtRef.current < SUPPRESS_CLICK_WINDOW_MS\n ) {\n return\n }\n\n lastTouchAtRef.current = Date.now()\n lastHandledAtRef.current = lastTouchAtRef.current\n lastHandledTypeRef.current = 'touch'\n lastHandledAnchorRef.current = anchor\n handleAnchor(anchor, e.nativeEvent)\n },\n [handleAnchor],\n )\n\n const handleContentPointerUp = useCallback(\n (e: ReactPointerEvent<HTMLElement>) => {\n if (e.defaultPrevented) return\n const target = e.target\n if (!(target instanceof Element)) return\n\n const anchor = target.closest('a')\n if (!anchor) return\n\n if (\n lastHandledAtRef.current &&\n lastHandledTypeRef.current === 'touch' &&\n lastHandledAnchorRef.current === anchor &&\n Date.now() - lastHandledAtRef.current < SUPPRESS_CLICK_WINDOW_MS\n ) {\n return\n }\n\n lastTouchAtRef.current = Date.now()\n lastHandledAtRef.current = lastTouchAtRef.current\n lastHandledTypeRef.current = 'pointer'\n lastHandledAnchorRef.current = anchor\n handleAnchor(anchor, e.nativeEvent)\n },\n [handleAnchor],\n )\n\n useEffect(() => {\n const container = containerRef?.current\n if (!container) return\n\n if (nativeLinkHandlers.has(container)) return\n\n /**\n * Capture-phase handler intercepting link activations on the container.\n * @param {NativeLinkEvent} e - The native click/touchend/pointerup event.\n * @returns {void} Nothing.\n */\n const handler = (e: NativeLinkEvent) => {\n try {\n if (isModifiedNativeEvent(e)) return\n const target = e.target\n if (!(target instanceof Element)) return\n\n const anchor = target.closest('a')\n if (!anchor) return\n\n const now = Date.now()\n if (\n lastHandledAtRef.current &&\n lastHandledAnchorRef.current === anchor &&\n now - lastHandledAtRef.current < SUPPRESS_CLICK_WINDOW_MS\n ) {\n return\n }\n\n lastHandledAtRef.current = now\n lastHandledTypeRef.current = isTouchLinkEvent(e) ? 'touch' : 'pointer'\n lastHandledAnchorRef.current = anchor\n handleAnchor(anchor, e)\n } catch {\n // Capture-phase handler must never throw into the host event loop.\n }\n }\n\n nativeLinkHandlers.set(container, handler)\n container.addEventListener('click', handler, true)\n container.addEventListener('touchend', handler, true)\n // Pointer events may be enabled in some webviews; harmless if unused.\n container.addEventListener('pointerup', handler, true)\n\n return () => {\n container.removeEventListener('click', handler, true)\n container.removeEventListener('touchend', handler, true)\n container.removeEventListener('pointerup', handler, true)\n nativeLinkHandlers.delete(container)\n }\n }, [containerRef, handleAnchor])\n\n return {\n prepareHtmlContent,\n handleContentClick,\n handleContentTouchEnd,\n handleContentPointerUp,\n }\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"react.es.mjs","names":[],"sources":["../../src/links/react/hasMouseEventProperties.ts","../../src/links/react/isModifiedNativeEvent.ts","../../src/links/react/isNativeLinkEvent.ts","../../src/links/react/isTouchLinkEvent.ts","../../src/links/react/nativeLinkHandlers.ts","../../src/links/react/tryStaffbaseContentOpenLink.ts","../../src/links/react/useInAppLinkHandling.ts"],"sourcesContent":["import type { NativeLinkEvent } from '../../types/links/NativeLinkEvent'\n\n/**\n * Whether a native link event exposes MouseEvent modifier properties.\n * @param {NativeLinkEvent} e - The native event.\n * @returns {boolean} True when the event has `button` and `metaKey`.\n */\nexport const hasMouseEventProperties = (\n e: NativeLinkEvent,\n): e is MouseEvent | PointerEvent => {\n return 'button' in e && 'metaKey' in e\n}\n","import type { NativeLinkEvent } from '../../types/links/NativeLinkEvent'\nimport { hasMouseEventProperties } from './hasMouseEventProperties'\n\n/**\n * Whether a native link event is \"modified\" and should be left to the browser\n * (default prevented, a non-primary mouse button, or a modifier key held).\n * @param {NativeLinkEvent} e - The native event.\n * @returns {boolean} True when the event should not be intercepted.\n */\nexport const isModifiedNativeEvent = (e: NativeLinkEvent): boolean => {\n const hasMouseProps = hasMouseEventProperties(e)\n const button = hasMouseProps && 'button' in e ? e.button : undefined\n const metaKey = hasMouseProps ? e.metaKey : false\n const ctrlKey = hasMouseProps ? e.ctrlKey : false\n const shiftKey = hasMouseProps ? e.shiftKey : false\n const altKey = hasMouseProps ? e.altKey : false\n\n return Boolean(\n e.defaultPrevented ||\n (typeof button === 'number' && button !== 0) ||\n metaKey ||\n ctrlKey ||\n shiftKey ||\n altKey,\n )\n}\n","import type { NativeLinkEvent } from '../../types/links/NativeLinkEvent'\n\n/**\n * Whether a value is a real native DOM event (as opposed to a React synthetic\n * event-like object), so the Staffbase content open-link path can be tried.\n * @param {NativeLinkEvent | { preventDefault?: () => void; stopPropagation?: () => void } | undefined} e - The candidate.\n * @returns {boolean} True when the value is a native Event.\n */\nexport const isNativeLinkEvent = (\n e:\n | { preventDefault?: () => void; stopPropagation?: () => void }\n | NativeLinkEvent\n | undefined,\n): e is NativeLinkEvent => {\n return e !== undefined && 'target' in e && e instanceof Event\n}\n","import type { NativeLinkEvent } from '../../types/links/NativeLinkEvent'\n\n/**\n * Whether a native link event is a TouchEvent (has `changedTouches`).\n * @param {NativeLinkEvent} e - The native event.\n * @returns {boolean} True when the event is a TouchEvent.\n */\nexport const isTouchLinkEvent = (e: NativeLinkEvent): e is TouchEvent => {\n return 'changedTouches' in e\n}\n","import type { NativeLinkEvent } from '../../types/links/NativeLinkEvent'\n\n/**\n * Tracks the capture-phase handler installed per container element, so the same\n * container is never wired twice and can be cleanly torn down on unmount.\n */\nexport const nativeLinkHandlers = new WeakMap<\n HTMLElement,\n (e: NativeLinkEvent) => void\n>()\n","import type { NativeLinkEvent } from '../../types/links/NativeLinkEvent'\nimport type { StaffbaseContentWindow } from '../../types/links/StaffbaseContentWindow'\n\n/**\n * Tries the host's own content open-link helper (the one Staffbase's widget\n * manager uses), to stay maximally aligned with platform behavior. Returns false\n * when the helper is absent or throws, so the caller can fall back.\n * @param {NativeLinkEvent} e - The originating native event.\n * @returns {boolean} True when the host helper handled the open.\n */\nexport const tryStaffbaseContentOpenLink = (e: NativeLinkEvent): boolean => {\n const staffbase = (window as unknown as StaffbaseContentWindow).staffbase\n const fn =\n staffbase?.content?.link?.openLink ||\n staffbase?.content?.links?.openLink ||\n staffbase?.content?.loader?.link?.openLink\n\n if (typeof fn !== 'function') return false\n\n try {\n fn({ useDefault: false }, e)\n return true\n } catch {\n // The host helper is best-effort; fall back to standard navigation on error.\n return false\n }\n}\n","import type {\n MouseEvent as ReactMouseEvent,\n PointerEvent as ReactPointerEvent,\n TouchEvent as ReactTouchEvent,\n} from 'react'\nimport { useCallback, useEffect, useRef } from 'react'\n\nimport type { NativeLinkEvent } from '../../types/links/NativeLinkEvent'\nimport type { UseInAppLinkHandlingOptions } from '../../types/links/UseInAppLinkHandlingOptions'\nimport type { UseInAppLinkHandlingReturn } from '../../types/links/UseInAppLinkHandlingReturn'\nimport { getInAppOpenLinkTarget } from '../getInAppOpenLinkTarget'\nimport { normalizeInAppLinks } from '../normalizeInAppLinks'\nimport { openStaffbaseAware } from '../openStaffbaseAware'\nimport { isModifiedNativeEvent } from './isModifiedNativeEvent'\nimport { isNativeLinkEvent } from './isNativeLinkEvent'\nimport { isTouchLinkEvent } from './isTouchLinkEvent'\nimport { nativeLinkHandlers } from './nativeLinkHandlers'\nimport { tryStaffbaseContentOpenLink } from './tryStaffbaseContentOpenLink'\n\nconst SUPPRESS_CLICK_WINDOW_MS = 700\n\n/**\n * Centralizes in-content link handling for Staffbase mobile/webview environments.\n *\n * Always attempts Staffbase-aware navigation (the host openLink when available,\n * otherwise same-tab navigation). It exposes React handlers for click/touchend/\n * pointerup and, when `containerRef` is given, installs a more reliable native\n * capture-phase handler. Touch/pointer/click are de-duplicated within a short\n * window so a single tap never fires navigation twice in iOS webviews.\n * @param {UseInAppLinkHandlingOptions} options - Origin, after-open callback and optional container ref.\n * @returns {UseInAppLinkHandlingReturn} prepareHtmlContent plus the React event handlers.\n */\nexport const useInAppLinkHandling = ({\n staffbaseOrigin,\n onAfterOpen,\n containerRef,\n}: UseInAppLinkHandlingOptions): UseInAppLinkHandlingReturn => {\n const lastTouchAtRef = useRef<number | null>(null)\n const lastHandledAtRef = useRef<number | null>(null)\n const lastHandledTypeRef = useRef<'touch' | 'pointer' | null>(null)\n const lastHandledAnchorRef = useRef<HTMLAnchorElement | null>(null)\n\n const prepareHtmlContent = useCallback(\n (content: HTMLElement): HTMLElement => {\n normalizeInAppLinks(content, staffbaseOrigin)\n return content\n },\n [staffbaseOrigin],\n )\n\n const handleAnchor = useCallback(\n (\n anchor: HTMLAnchorElement,\n e?:\n | { preventDefault?: () => void; stopPropagation?: () => void }\n | NativeLinkEvent,\n ) => {\n const linkToOpen = getInAppOpenLinkTarget(\n anchor.getAttribute('href') ?? anchor.href ?? null,\n staffbaseOrigin,\n )\n if (!linkToOpen) return\n\n // Handle both React synthetic event-like objects and native events.\n if (e && 'preventDefault' in e) {\n e.preventDefault?.()\n e.stopPropagation?.()\n }\n\n // Prefer Staffbase's own content link handler when this is a native event.\n if (isNativeLinkEvent(e)) {\n // Touch events in some webviews don't trigger Staffbase openLink reliably.\n if (isTouchLinkEvent(e)) {\n openStaffbaseAware(linkToOpen)\n onAfterOpen?.()\n return\n }\n if (!tryStaffbaseContentOpenLink(e)) {\n openStaffbaseAware(linkToOpen)\n }\n } else {\n openStaffbaseAware(linkToOpen)\n }\n onAfterOpen?.()\n },\n [onAfterOpen, staffbaseOrigin],\n )\n\n const handleContentClick = useCallback(\n (e: ReactMouseEvent<HTMLElement>) => {\n if (e.defaultPrevented) return\n if (\n lastTouchAtRef.current &&\n Date.now() - lastTouchAtRef.current < SUPPRESS_CLICK_WINDOW_MS\n ) {\n return\n }\n\n const target = e.target\n if (!(target instanceof Element)) return\n\n const anchor = target.closest('a')\n if (!anchor) return\n handleAnchor(anchor, e)\n },\n [handleAnchor],\n )\n\n const handleContentTouchEnd = useCallback(\n (e: ReactTouchEvent<HTMLElement>) => {\n if (e.defaultPrevented) return\n const target = e.target\n if (!(target instanceof Element)) return\n\n const anchor = target.closest('a')\n if (!anchor) return\n\n if (\n lastHandledAtRef.current &&\n lastHandledTypeRef.current === 'pointer' &&\n lastHandledAnchorRef.current === anchor &&\n Date.now() - lastHandledAtRef.current < SUPPRESS_CLICK_WINDOW_MS\n ) {\n return\n }\n\n lastTouchAtRef.current = Date.now()\n lastHandledAtRef.current = lastTouchAtRef.current\n lastHandledTypeRef.current = 'touch'\n lastHandledAnchorRef.current = anchor\n handleAnchor(anchor, e.nativeEvent)\n },\n [handleAnchor],\n )\n\n const handleContentPointerUp = useCallback(\n (e: ReactPointerEvent<HTMLElement>) => {\n if (e.defaultPrevented) return\n const target = e.target\n if (!(target instanceof Element)) return\n\n const anchor = target.closest('a')\n if (!anchor) return\n\n if (\n lastHandledAtRef.current &&\n lastHandledTypeRef.current === 'touch' &&\n lastHandledAnchorRef.current === anchor &&\n Date.now() - lastHandledAtRef.current < SUPPRESS_CLICK_WINDOW_MS\n ) {\n return\n }\n\n lastTouchAtRef.current = Date.now()\n lastHandledAtRef.current = lastTouchAtRef.current\n lastHandledTypeRef.current = 'pointer'\n lastHandledAnchorRef.current = anchor\n handleAnchor(anchor, e.nativeEvent)\n },\n [handleAnchor],\n )\n\n useEffect(() => {\n const container = containerRef?.current\n if (!container) return\n\n if (nativeLinkHandlers.has(container)) return\n\n /**\n * Capture-phase handler intercepting link activations on the container.\n * @param {NativeLinkEvent} e - The native click/touchend/pointerup event.\n * @returns {void} Nothing.\n */\n const handler = (e: NativeLinkEvent) => {\n try {\n if (isModifiedNativeEvent(e)) return\n const target = e.target\n if (!(target instanceof Element)) return\n\n const anchor = target.closest('a')\n if (!anchor) return\n\n const now = Date.now()\n if (\n lastHandledAtRef.current &&\n lastHandledAnchorRef.current === anchor &&\n now - lastHandledAtRef.current < SUPPRESS_CLICK_WINDOW_MS\n ) {\n return\n }\n\n lastHandledAtRef.current = now\n lastHandledTypeRef.current = isTouchLinkEvent(e) ? 'touch' : 'pointer'\n lastHandledAnchorRef.current = anchor\n handleAnchor(anchor, e)\n } catch {\n // Capture-phase handler must never throw into the host event loop.\n }\n }\n\n nativeLinkHandlers.set(container, handler)\n container.addEventListener('click', handler, true)\n container.addEventListener('touchend', handler, true)\n // Pointer events may be enabled in some webviews; harmless if unused.\n container.addEventListener('pointerup', handler, true)\n\n return () => {\n container.removeEventListener('click', handler, true)\n container.removeEventListener('touchend', handler, true)\n container.removeEventListener('pointerup', handler, true)\n nativeLinkHandlers.delete(container)\n }\n }, [containerRef, handleAnchor])\n\n return {\n prepareHtmlContent,\n handleContentClick,\n handleContentTouchEnd,\n handleContentPointerUp,\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"react.es.mjs","names":[],"sources":["../../src/links/react/hasMouseEventProperties.ts","../../src/links/react/isModifiedNativeEvent.ts","../../src/links/react/isNativeLinkEvent.ts","../../src/links/react/isTouchLinkEvent.ts","../../src/links/react/nativeLinkHandlers.ts","../../src/links/react/tryStaffbaseContentOpenLink.ts","../../src/links/react/useInAppLinkHandling.ts"],"sourcesContent":["import type { NativeLinkEvent } from '../../types/links/NativeLinkEvent'\n\n/**\n * Whether a native link event exposes MouseEvent modifier properties.\n * @param {NativeLinkEvent} e - The native event.\n * @returns {boolean} True when the event has `button` and `metaKey`.\n */\nexport const hasMouseEventProperties = (\n e: NativeLinkEvent,\n): e is MouseEvent | PointerEvent => {\n return 'button' in e && 'metaKey' in e\n}\n","import type { NativeLinkEvent } from '../../types/links/NativeLinkEvent'\nimport { hasMouseEventProperties } from './hasMouseEventProperties'\n\n/**\n * Whether a native link event is \"modified\" and should be left to the browser\n * (default prevented, a non-primary mouse button, or a modifier key held).\n * @param {NativeLinkEvent} e - The native event.\n * @returns {boolean} True when the event should not be intercepted.\n */\nexport const isModifiedNativeEvent = (e: NativeLinkEvent): boolean => {\n const hasMouseProps = hasMouseEventProperties(e)\n const button = hasMouseProps && 'button' in e ? e.button : undefined\n const metaKey = hasMouseProps ? e.metaKey : false\n const ctrlKey = hasMouseProps ? e.ctrlKey : false\n const shiftKey = hasMouseProps ? e.shiftKey : false\n const altKey = hasMouseProps ? e.altKey : false\n\n return Boolean(\n e.defaultPrevented ||\n (typeof button === 'number' && button !== 0) ||\n metaKey ||\n ctrlKey ||\n shiftKey ||\n altKey,\n )\n}\n","import type { NativeLinkEvent } from '../../types/links/NativeLinkEvent'\n\n/**\n * Whether a value is a real native DOM event (as opposed to a React synthetic\n * event-like object), so the Staffbase content open-link path can be tried.\n * @param {NativeLinkEvent | { preventDefault?: () => void; stopPropagation?: () => void } | undefined} e - The candidate.\n * @returns {boolean} True when the value is a native Event.\n */\nexport const isNativeLinkEvent = (\n e:\n | { preventDefault?: () => void; stopPropagation?: () => void }\n | NativeLinkEvent\n | undefined,\n): e is NativeLinkEvent => {\n return e !== undefined && 'target' in e && e instanceof Event\n}\n","import type { NativeLinkEvent } from '../../types/links/NativeLinkEvent'\n\n/**\n * Whether a native link event is a TouchEvent (has `changedTouches`).\n * @param {NativeLinkEvent} e - The native event.\n * @returns {boolean} True when the event is a TouchEvent.\n */\nexport const isTouchLinkEvent = (e: NativeLinkEvent): e is TouchEvent => {\n return 'changedTouches' in e\n}\n","import type { NativeLinkEvent } from '../../types/links/NativeLinkEvent'\n\n/**\n * Tracks the capture-phase handler installed per container element, so the same\n * container is never wired twice and can be cleanly torn down on unmount.\n */\nexport const nativeLinkHandlers = new WeakMap<\n HTMLElement,\n (e: NativeLinkEvent) => void\n>()\n","import type { NativeLinkEvent } from '../../types/links/NativeLinkEvent'\nimport type { StaffbaseContentWindow } from '../../types/links/StaffbaseContentWindow'\n\n/**\n * Tries the host's own content open-link helper (the one Staffbase's widget\n * manager uses), to stay maximally aligned with platform behavior. Returns false\n * when the helper is absent or throws, so the caller can fall back.\n * @param {NativeLinkEvent} e - The originating native event.\n * @returns {boolean} True when the host helper handled the open.\n */\nexport const tryStaffbaseContentOpenLink = (e: NativeLinkEvent): boolean => {\n const staffbase = (window as unknown as StaffbaseContentWindow).staffbase\n const fn =\n staffbase?.content?.link?.openLink ||\n staffbase?.content?.links?.openLink ||\n staffbase?.content?.loader?.link?.openLink\n\n if (typeof fn !== 'function') return false\n\n try {\n fn({ useDefault: false }, e)\n return true\n } catch {\n // The host helper is best-effort; fall back to standard navigation on error.\n return false\n }\n}\n","import type {\n MouseEvent as ReactMouseEvent,\n PointerEvent as ReactPointerEvent,\n TouchEvent as ReactTouchEvent,\n} from 'react'\nimport { useCallback, useEffect, useRef } from 'react'\n\nimport type { NativeLinkEvent } from '../../types/links/NativeLinkEvent'\nimport type { UseInAppLinkHandlingOptions } from '../../types/links/UseInAppLinkHandlingOptions'\nimport type { UseInAppLinkHandlingReturn } from '../../types/links/UseInAppLinkHandlingReturn'\nimport { getInAppOpenLinkTarget } from '../getInAppOpenLinkTarget'\nimport { normalizeInAppLinks } from '../normalizeInAppLinks'\nimport { openStaffbaseAware } from '../openStaffbaseAware'\nimport { isModifiedNativeEvent } from './isModifiedNativeEvent'\nimport { isNativeLinkEvent } from './isNativeLinkEvent'\nimport { isTouchLinkEvent } from './isTouchLinkEvent'\nimport { nativeLinkHandlers } from './nativeLinkHandlers'\nimport { tryStaffbaseContentOpenLink } from './tryStaffbaseContentOpenLink'\n\nconst SUPPRESS_CLICK_WINDOW_MS = 700\n\n/**\n * Centralizes in-content link handling for Staffbase mobile/webview environments.\n *\n * Always attempts Staffbase-aware navigation (the host openLink when available,\n * otherwise same-tab navigation). It exposes React handlers for click/touchend/\n * pointerup and, when `containerRef` is given, installs a more reliable native\n * capture-phase handler. Touch/pointer/click are de-duplicated within a short\n * window so a single tap never fires navigation twice in iOS webviews.\n * @param {UseInAppLinkHandlingOptions} options - Origin, after-open callback and optional container ref.\n * @returns {UseInAppLinkHandlingReturn} prepareHtmlContent plus the React event handlers.\n */\nexport const useInAppLinkHandling = ({\n staffbaseOrigin,\n onAfterOpen,\n containerRef,\n}: UseInAppLinkHandlingOptions): UseInAppLinkHandlingReturn => {\n const lastTouchAtRef = useRef<number | null>(null)\n const lastHandledAtRef = useRef<number | null>(null)\n const lastHandledTypeRef = useRef<'touch' | 'pointer' | null>(null)\n const lastHandledAnchorRef = useRef<HTMLAnchorElement | null>(null)\n\n const prepareHtmlContent = useCallback(\n (content: HTMLElement): HTMLElement => {\n normalizeInAppLinks(content, staffbaseOrigin)\n return content\n },\n [staffbaseOrigin],\n )\n\n const handleAnchor = useCallback(\n (\n anchor: HTMLAnchorElement,\n e?:\n | { preventDefault?: () => void; stopPropagation?: () => void }\n | NativeLinkEvent,\n ) => {\n const linkToOpen = getInAppOpenLinkTarget(\n anchor.getAttribute('href') ?? anchor.href ?? null,\n staffbaseOrigin,\n )\n if (!linkToOpen) return\n\n // Handle both React synthetic event-like objects and native events.\n if (e && 'preventDefault' in e) {\n e.preventDefault?.()\n e.stopPropagation?.()\n }\n\n // Prefer Staffbase's own content link handler when this is a native event.\n if (isNativeLinkEvent(e)) {\n // Touch events in some webviews don't trigger Staffbase openLink reliably.\n if (isTouchLinkEvent(e)) {\n openStaffbaseAware(linkToOpen)\n onAfterOpen?.()\n return\n }\n if (!tryStaffbaseContentOpenLink(e)) {\n openStaffbaseAware(linkToOpen)\n }\n } else {\n openStaffbaseAware(linkToOpen)\n }\n onAfterOpen?.()\n },\n [onAfterOpen, staffbaseOrigin],\n )\n\n const handleContentClick = useCallback(\n (e: ReactMouseEvent<HTMLElement>) => {\n if (e.defaultPrevented) return\n if (\n lastTouchAtRef.current &&\n Date.now() - lastTouchAtRef.current < SUPPRESS_CLICK_WINDOW_MS\n ) {\n return\n }\n\n const target = e.target\n if (!(target instanceof Element)) return\n\n const anchor = target.closest('a')\n if (!anchor) return\n handleAnchor(anchor, e)\n },\n [handleAnchor],\n )\n\n const handleContentTouchEnd = useCallback(\n (e: ReactTouchEvent<HTMLElement>) => {\n if (e.defaultPrevented) return\n const target = e.target\n if (!(target instanceof Element)) return\n\n const anchor = target.closest('a')\n if (!anchor) return\n\n if (\n lastHandledAtRef.current &&\n lastHandledTypeRef.current === 'pointer' &&\n lastHandledAnchorRef.current === anchor &&\n Date.now() - lastHandledAtRef.current < SUPPRESS_CLICK_WINDOW_MS\n ) {\n return\n }\n\n lastTouchAtRef.current = Date.now()\n lastHandledAtRef.current = lastTouchAtRef.current\n lastHandledTypeRef.current = 'touch'\n lastHandledAnchorRef.current = anchor\n handleAnchor(anchor, e.nativeEvent)\n },\n [handleAnchor],\n )\n\n const handleContentPointerUp = useCallback(\n (e: ReactPointerEvent<HTMLElement>) => {\n if (e.defaultPrevented) return\n const target = e.target\n if (!(target instanceof Element)) return\n\n const anchor = target.closest('a')\n if (!anchor) return\n\n if (\n lastHandledAtRef.current &&\n lastHandledTypeRef.current === 'touch' &&\n lastHandledAnchorRef.current === anchor &&\n Date.now() - lastHandledAtRef.current < SUPPRESS_CLICK_WINDOW_MS\n ) {\n return\n }\n\n lastTouchAtRef.current = Date.now()\n lastHandledAtRef.current = lastTouchAtRef.current\n lastHandledTypeRef.current = 'pointer'\n lastHandledAnchorRef.current = anchor\n handleAnchor(anchor, e.nativeEvent)\n },\n [handleAnchor],\n )\n\n useEffect(() => {\n const container = containerRef?.current\n if (!container) return\n\n if (nativeLinkHandlers.has(container)) return\n\n /**\n * Capture-phase handler intercepting link activations on the container.\n * @param {NativeLinkEvent} e - The native click/touchend/pointerup event.\n * @returns {void} Nothing.\n */\n const handler = (e: NativeLinkEvent) => {\n try {\n if (isModifiedNativeEvent(e)) return\n const target = e.target\n if (!(target instanceof Element)) return\n\n const anchor = target.closest('a')\n if (!anchor) return\n\n const now = Date.now()\n if (\n lastHandledAtRef.current &&\n lastHandledAnchorRef.current === anchor &&\n now - lastHandledAtRef.current < SUPPRESS_CLICK_WINDOW_MS\n ) {\n return\n }\n\n lastHandledAtRef.current = now\n lastHandledTypeRef.current = isTouchLinkEvent(e) ? 'touch' : 'pointer'\n lastHandledAnchorRef.current = anchor\n handleAnchor(anchor, e)\n } catch {\n // Capture-phase handler must never throw into the host event loop.\n }\n }\n\n nativeLinkHandlers.set(container, handler)\n container.addEventListener('click', handler, true)\n container.addEventListener('touchend', handler, true)\n // Pointer events may be enabled in some webviews; harmless if unused.\n container.addEventListener('pointerup', handler, true)\n\n return () => {\n container.removeEventListener('click', handler, true)\n container.removeEventListener('touchend', handler, true)\n container.removeEventListener('pointerup', handler, true)\n nativeLinkHandlers.delete(container)\n }\n }, [containerRef, handleAnchor])\n\n return {\n prepareHtmlContent,\n handleContentClick,\n handleContentTouchEnd,\n handleContentPointerUp,\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("../chunk-CMqjfN_6.js"),t=require("../injectShadowStyles-SjxVBpXH.js");let n=require("react"),r=require("@emotion/cache");r=e.t(r);var i=(e,t)=>{let n=e.getElementById(t);if(n instanceof HTMLDivElement)return n;let r=document.createElement(`div`);return r.id=t,e.appendChild(r),r},a=(e,t)=>{let n=e.getElementById(t);if(n instanceof HTMLMetaElement)return n;let r=document.createElement(`meta`);return r.id=t,r.setAttribute(`name`,`emotion-insertion-point`),e.appendChild(r),r},o=`sbu-shadow-styles`,s=`sbu-react-root`,c=`sbu-portal-root`,l=`sbu-emotion-insertion-point`,u=(e,{cacheKey:n,cssText:u})=>{let d=e.shadowRoot??e.attachShadow({mode:`open`}),f=i(d,`${s}-${n}`),p=i(d,`${c}-${n}`),m=a(d,`${l}-${n}`);return t.t(d,u,`${o}-${n}`),{shadowRoot:d,reactMountEl:f,portalContainerEl:p,emotionCache:(0,r.default)({key:n,container:d,insertionPoint:m})}},d=(0,n.createContext)(null),f=({portalContainer:e,children:t})=>(0,n.createElement)(d.Provider,{value:e},t),p=()=>(0,n.useContext)(d);exports.PortalContainerProvider=f,exports.ensureShadowMount=u,exports.usePortalContainer=p;
2
+ //# sourceMappingURL=react.cjs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"react.cjs.js","names":[],"sources":["../../src/shadow/react/getOrCreateDiv.ts","../../src/shadow/react/getOrCreateEmotionInsertionPoint.ts","../../src/shadow/react/ensureShadowMount.ts","../../src/shadow/react/PortalContainerContext.ts","../../src/shadow/react/PortalContainerProvider.ts","../../src/shadow/react/usePortalContainer.ts"],"sourcesContent":["/**\n * Returns the shadow-root child `<div>` with the given id, creating it if absent.\n * @param {ShadowRoot} root - The shadow root to search/append within.\n * @param {string} id - The element id.\n * @returns {HTMLDivElement} The existing or newly created div.\n */\nexport const getOrCreateDiv = (\n root: ShadowRoot,\n id: string,\n): HTMLDivElement => {\n const existing = root.getElementById(id)\n if (existing instanceof HTMLDivElement) return existing\n\n const el = document.createElement('div')\n el.id = id\n root.appendChild(el)\n return el\n}\n","/**\n * Returns the shadow-root `<meta name=\"emotion-insertion-point\">` with the given\n * id, creating it if absent. Keeps Emotion's styles ordered before shadow content.\n * @param {ShadowRoot} root - The shadow root to search/append within.\n * @param {string} id - The element id.\n * @returns {HTMLMetaElement} The existing or newly created insertion-point meta.\n */\nexport const getOrCreateEmotionInsertionPoint = (\n root: ShadowRoot,\n id: string,\n): HTMLMetaElement => {\n const existing = root.getElementById(id)\n if (existing instanceof HTMLMetaElement) return existing\n\n const el = document.createElement('meta')\n el.id = id\n el.setAttribute('name', 'emotion-insertion-point')\n root.appendChild(el)\n return el\n}\n","import createCache from '@emotion/cache'\n\nimport type { EnsureShadowMountOptions } from '../../types/shadow/EnsureShadowMountOptions'\nimport type { ShadowMount } from '../../types/shadow/ShadowMount'\nimport { injectShadowStyles } from '../injectShadowStyles'\nimport { getOrCreateDiv } from './getOrCreateDiv'\nimport { getOrCreateEmotionInsertionPoint } from './getOrCreateEmotionInsertionPoint'\n\nconst STYLE_ELEMENT_ID = 'sbu-shadow-styles'\nconst REACT_MOUNT_ID = 'sbu-react-root'\nconst PORTAL_CONTAINER_ID = 'sbu-portal-root'\nconst EMOTION_INSERTION_POINT_ID = 'sbu-emotion-insertion-point'\n\n/**\n * Ensures a ShadowRoot mount exists on the host element for a widget.\n *\n * Creates stable React-mount and portal-container nodes, injects the widget's CSS\n * into the shadow root only (never into the document), and configures an Emotion\n * cache scoped to the shadow root. All internal ids are namespaced by `cacheKey`\n * so editor and runtime mounts on the same host never collide.\n * @param {HTMLElement} host - The host element to attach the shadow root to.\n * @param {EnsureShadowMountOptions} options - The cache key and the widget's CSS text.\n * @returns {ShadowMount} The shadow root, mount nodes and the scoped Emotion cache.\n */\nexport const ensureShadowMount = (\n host: HTMLElement,\n { cacheKey, cssText }: EnsureShadowMountOptions,\n): ShadowMount => {\n const shadowRoot = host.shadowRoot ?? host.attachShadow({ mode: 'open' })\n\n const reactMountEl = getOrCreateDiv(\n shadowRoot,\n `${REACT_MOUNT_ID}-${cacheKey}`,\n )\n const portalContainerEl = getOrCreateDiv(\n shadowRoot,\n `${PORTAL_CONTAINER_ID}-${cacheKey}`,\n )\n const insertionPoint = getOrCreateEmotionInsertionPoint(\n shadowRoot,\n `${EMOTION_INSERTION_POINT_ID}-${cacheKey}`,\n )\n\n injectShadowStyles(shadowRoot, cssText, `${STYLE_ELEMENT_ID}-${cacheKey}`)\n\n // Emotion's `container` is typed as HTMLElement but supports ShadowRoot targets\n // at runtime; the localized cast keeps the rest of the call type-safe.\n const emotionCache = createCache({\n key: cacheKey,\n container: shadowRoot as unknown as HTMLElement,\n insertionPoint,\n })\n\n return { shadowRoot, reactMountEl, portalContainerEl, emotionCache }\n}\n","import { createContext } from 'react'\n\n/**\n * Holds the DOM element portalled components should render into, so portals stay\n * inside the widget's ShadowRoot (preserving style isolation).\n */\nexport const PortalContainerContext = createContext<HTMLElement | null>(null)\n","import type { ReactElement } from 'react'\nimport { createElement } from 'react'\n\nimport type { PortalContainerProviderProps } from '../../types/shadow/PortalContainerProviderProps'\nimport { PortalContainerContext } from './PortalContainerContext'\n\n/**\n * Provides the portal container element to descendants. Built with createElement\n * (no JSX) so the library has no JSX-runtime build coupling.\n * @param {PortalContainerProviderProps} props - The portal container and children.\n * @returns {ReactElement} The provider element wrapping the children.\n */\nexport const PortalContainerProvider = ({\n portalContainer,\n children,\n}: PortalContainerProviderProps): ReactElement =>\n createElement(\n PortalContainerContext.Provider,\n { value: portalContainer },\n children,\n )\n","import { useContext } from 'react'\n\nimport { PortalContainerContext } from './PortalContainerContext'\n\n/**\n * Returns the portal container element used to keep portals inside the ShadowRoot.\n * @returns {HTMLElement | null} The portal container, or null when not provided.\n */\nexport const usePortalContainer = (): HTMLElement | null => {\n return useContext(PortalContainerContext)\n}\n"],"mappings":"sNAMA,IAAa,GACX,EACA,IACmB,CACnB,IAAM,EAAW,EAAK,eAAe,CAAE,EACvC,GAAI,aAAoB,eAAgB,OAAO,EAE/C,IAAM,EAAK,SAAS,cAAc,KAAK,EAGvC,MAFA,GAAG,GAAK,EACR,EAAK,YAAY,CAAE,EACZ,CACT,ECVa,GACX,EACA,IACoB,CACpB,IAAM,EAAW,EAAK,eAAe,CAAE,EACvC,GAAI,aAAoB,gBAAiB,OAAO,EAEhD,IAAM,EAAK,SAAS,cAAc,MAAM,EAIxC,MAHA,GAAG,GAAK,EACR,EAAG,aAAa,OAAQ,yBAAyB,EACjD,EAAK,YAAY,CAAE,EACZ,CACT,ECXM,EAAmB,oBACnB,EAAiB,iBACjB,EAAsB,kBACtB,EAA6B,8BAatB,GACX,EACA,CAAE,WAAU,aACI,CAChB,IAAM,EAAa,EAAK,YAAc,EAAK,aAAa,CAAE,KAAM,MAAO,CAAC,EAElE,EAAe,EACnB,EACA,GAAG,EAAe,GAAG,GACvB,EACM,EAAoB,EACxB,EACA,GAAG,EAAoB,GAAG,GAC5B,EACM,EAAiB,EACrB,EACA,GAAG,EAA2B,GAAG,GACnC,EAYA,OAVA,EAAA,EAAmB,EAAY,EAAS,GAAG,EAAiB,GAAG,GAAU,EAUlE,CAAE,aAAY,eAAc,oBAAmB,cAAA,EAAA,EAAA,SANrB,CAC/B,IAAK,EACL,UAAW,EACX,gBACF,CAEsD,CAAa,CACrE,EChDa,GAAA,EAAA,EAAA,eAA2D,IAAI,ECM/D,GAA2B,CACtC,kBACA,eAAA,EAAA,EAAA,eAGE,EAAuB,SACvB,CAAE,MAAO,CAAgB,EACzB,CACF,ECZW,OACX,EAAA,EAAA,YAAkB,CAAsB"}
@@ -0,0 +1,31 @@
1
+ import { t as e } from "../injectShadowStyles-C40qdwKI.mjs";
2
+ import { createContext as t, createElement as n, useContext as r } from "react";
3
+ import i from "@emotion/cache";
4
+ //#region src/shadow/react/getOrCreateDiv.ts
5
+ var a = (e, t) => {
6
+ let n = e.getElementById(t);
7
+ if (n instanceof HTMLDivElement) return n;
8
+ let r = document.createElement("div");
9
+ return r.id = t, e.appendChild(r), r;
10
+ }, o = (e, t) => {
11
+ let n = e.getElementById(t);
12
+ if (n instanceof HTMLMetaElement) return n;
13
+ let r = document.createElement("meta");
14
+ return r.id = t, r.setAttribute("name", "emotion-insertion-point"), e.appendChild(r), r;
15
+ }, s = "sbu-shadow-styles", c = "sbu-react-root", l = "sbu-portal-root", u = "sbu-emotion-insertion-point", d = (t, { cacheKey: n, cssText: r }) => {
16
+ let d = t.shadowRoot ?? t.attachShadow({ mode: "open" }), f = a(d, `${c}-${n}`), p = a(d, `${l}-${n}`), m = o(d, `${u}-${n}`);
17
+ return e(d, r, `${s}-${n}`), {
18
+ shadowRoot: d,
19
+ reactMountEl: f,
20
+ portalContainerEl: p,
21
+ emotionCache: i({
22
+ key: n,
23
+ container: d,
24
+ insertionPoint: m
25
+ })
26
+ };
27
+ }, f = t(null), p = ({ portalContainer: e, children: t }) => n(f.Provider, { value: e }, t), m = () => r(f);
28
+ //#endregion
29
+ export { p as PortalContainerProvider, d as ensureShadowMount, m as usePortalContainer };
30
+
31
+ //# sourceMappingURL=react.es.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"react.es.mjs","names":[],"sources":["../../src/shadow/react/getOrCreateDiv.ts","../../src/shadow/react/getOrCreateEmotionInsertionPoint.ts","../../src/shadow/react/ensureShadowMount.ts","../../src/shadow/react/PortalContainerContext.ts","../../src/shadow/react/PortalContainerProvider.ts","../../src/shadow/react/usePortalContainer.ts"],"sourcesContent":["/**\n * Returns the shadow-root child `<div>` with the given id, creating it if absent.\n * @param {ShadowRoot} root - The shadow root to search/append within.\n * @param {string} id - The element id.\n * @returns {HTMLDivElement} The existing or newly created div.\n */\nexport const getOrCreateDiv = (\n root: ShadowRoot,\n id: string,\n): HTMLDivElement => {\n const existing = root.getElementById(id)\n if (existing instanceof HTMLDivElement) return existing\n\n const el = document.createElement('div')\n el.id = id\n root.appendChild(el)\n return el\n}\n","/**\n * Returns the shadow-root `<meta name=\"emotion-insertion-point\">` with the given\n * id, creating it if absent. Keeps Emotion's styles ordered before shadow content.\n * @param {ShadowRoot} root - The shadow root to search/append within.\n * @param {string} id - The element id.\n * @returns {HTMLMetaElement} The existing or newly created insertion-point meta.\n */\nexport const getOrCreateEmotionInsertionPoint = (\n root: ShadowRoot,\n id: string,\n): HTMLMetaElement => {\n const existing = root.getElementById(id)\n if (existing instanceof HTMLMetaElement) return existing\n\n const el = document.createElement('meta')\n el.id = id\n el.setAttribute('name', 'emotion-insertion-point')\n root.appendChild(el)\n return el\n}\n","import createCache from '@emotion/cache'\n\nimport type { EnsureShadowMountOptions } from '../../types/shadow/EnsureShadowMountOptions'\nimport type { ShadowMount } from '../../types/shadow/ShadowMount'\nimport { injectShadowStyles } from '../injectShadowStyles'\nimport { getOrCreateDiv } from './getOrCreateDiv'\nimport { getOrCreateEmotionInsertionPoint } from './getOrCreateEmotionInsertionPoint'\n\nconst STYLE_ELEMENT_ID = 'sbu-shadow-styles'\nconst REACT_MOUNT_ID = 'sbu-react-root'\nconst PORTAL_CONTAINER_ID = 'sbu-portal-root'\nconst EMOTION_INSERTION_POINT_ID = 'sbu-emotion-insertion-point'\n\n/**\n * Ensures a ShadowRoot mount exists on the host element for a widget.\n *\n * Creates stable React-mount and portal-container nodes, injects the widget's CSS\n * into the shadow root only (never into the document), and configures an Emotion\n * cache scoped to the shadow root. All internal ids are namespaced by `cacheKey`\n * so editor and runtime mounts on the same host never collide.\n * @param {HTMLElement} host - The host element to attach the shadow root to.\n * @param {EnsureShadowMountOptions} options - The cache key and the widget's CSS text.\n * @returns {ShadowMount} The shadow root, mount nodes and the scoped Emotion cache.\n */\nexport const ensureShadowMount = (\n host: HTMLElement,\n { cacheKey, cssText }: EnsureShadowMountOptions,\n): ShadowMount => {\n const shadowRoot = host.shadowRoot ?? host.attachShadow({ mode: 'open' })\n\n const reactMountEl = getOrCreateDiv(\n shadowRoot,\n `${REACT_MOUNT_ID}-${cacheKey}`,\n )\n const portalContainerEl = getOrCreateDiv(\n shadowRoot,\n `${PORTAL_CONTAINER_ID}-${cacheKey}`,\n )\n const insertionPoint = getOrCreateEmotionInsertionPoint(\n shadowRoot,\n `${EMOTION_INSERTION_POINT_ID}-${cacheKey}`,\n )\n\n injectShadowStyles(shadowRoot, cssText, `${STYLE_ELEMENT_ID}-${cacheKey}`)\n\n // Emotion's `container` is typed as HTMLElement but supports ShadowRoot targets\n // at runtime; the localized cast keeps the rest of the call type-safe.\n const emotionCache = createCache({\n key: cacheKey,\n container: shadowRoot as unknown as HTMLElement,\n insertionPoint,\n })\n\n return { shadowRoot, reactMountEl, portalContainerEl, emotionCache }\n}\n","import { createContext } from 'react'\n\n/**\n * Holds the DOM element portalled components should render into, so portals stay\n * inside the widget's ShadowRoot (preserving style isolation).\n */\nexport const PortalContainerContext = createContext<HTMLElement | null>(null)\n","import type { ReactElement } from 'react'\nimport { createElement } from 'react'\n\nimport type { PortalContainerProviderProps } from '../../types/shadow/PortalContainerProviderProps'\nimport { PortalContainerContext } from './PortalContainerContext'\n\n/**\n * Provides the portal container element to descendants. Built with createElement\n * (no JSX) so the library has no JSX-runtime build coupling.\n * @param {PortalContainerProviderProps} props - The portal container and children.\n * @returns {ReactElement} The provider element wrapping the children.\n */\nexport const PortalContainerProvider = ({\n portalContainer,\n children,\n}: PortalContainerProviderProps): ReactElement =>\n createElement(\n PortalContainerContext.Provider,\n { value: portalContainer },\n children,\n )\n","import { useContext } from 'react'\n\nimport { PortalContainerContext } from './PortalContainerContext'\n\n/**\n * Returns the portal container element used to keep portals inside the ShadowRoot.\n * @returns {HTMLElement | null} The portal container, or null when not provided.\n */\nexport const usePortalContainer = (): HTMLElement | null => {\n return useContext(PortalContainerContext)\n}\n"],"mappings":";;;;AAMA,IAAa,KACX,GACA,MACmB;CACnB,IAAM,IAAW,EAAK,eAAe,CAAE;CACvC,IAAI,aAAoB,gBAAgB,OAAO;CAE/C,IAAM,IAAK,SAAS,cAAc,KAAK;CAGvC,OAFA,EAAG,KAAK,GACR,EAAK,YAAY,CAAE,GACZ;AACT,GCVa,KACX,GACA,MACoB;CACpB,IAAM,IAAW,EAAK,eAAe,CAAE;CACvC,IAAI,aAAoB,iBAAiB,OAAO;CAEhD,IAAM,IAAK,SAAS,cAAc,MAAM;CAIxC,OAHA,EAAG,KAAK,GACR,EAAG,aAAa,QAAQ,yBAAyB,GACjD,EAAK,YAAY,CAAE,GACZ;AACT,GCXM,IAAmB,qBACnB,IAAiB,kBACjB,IAAsB,mBACtB,IAA6B,+BAatB,KACX,GACA,EAAE,aAAU,iBACI;CAChB,IAAM,IAAa,EAAK,cAAc,EAAK,aAAa,EAAE,MAAM,OAAO,CAAC,GAElE,IAAe,EACnB,GACA,GAAG,EAAe,GAAG,GACvB,GACM,IAAoB,EACxB,GACA,GAAG,EAAoB,GAAG,GAC5B,GACM,IAAiB,EACrB,GACA,GAAG,EAA2B,GAAG,GACnC;CAYA,OAVA,EAAmB,GAAY,GAAS,GAAG,EAAiB,GAAG,GAAU,GAUlE;EAAE;EAAY;EAAc;EAAmB,cANjC,EAAY;GAC/B,KAAK;GACL,WAAW;GACX;EACF,CAEsD;CAAa;AACrE,GChDa,IAAyB,EAAkC,IAAI,GCM/D,KAA2B,EACtC,oBACA,kBAEA,EACE,EAAuB,UACvB,EAAE,OAAO,EAAgB,GACzB,CACF,GCZW,UACJ,EAAW,CAAsB"}
@@ -0,0 +1 @@
1
+ Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("./injectShadowStyles-SjxVBpXH.js");exports.injectShadowStyles=e.t,exports.isShadowRoot=e.n;
@@ -0,0 +1,2 @@
1
+ import { n as e, t } from "./injectShadowStyles-C40qdwKI.mjs";
2
+ export { t as injectShadowStyles, e as isShadowRoot };
@@ -1,6 +1,6 @@
1
+ export type { ResolveLocalizedContentOptions } from '../types/content/ResolveLocalizedContentOptions';
1
2
  export { detectEditorLanguage } from './detectEditorLanguage';
2
3
  export { detectPreviewLanguage } from './detectPreviewLanguage';
3
4
  export { resolveActiveLanguage } from './resolveActiveLanguage';
4
5
  export { resolveLocalizedContent } from './resolveLocalizedContent';
5
- export type { ResolveLocalizedContentOptions } from '../types/content/ResolveLocalizedContentOptions';
6
6
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/content/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAA;AAC7D,OAAO,EAAE,qBAAqB,EAAE,MAAM,yBAAyB,CAAA;AAC/D,OAAO,EAAE,qBAAqB,EAAE,MAAM,yBAAyB,CAAA;AAC/D,OAAO,EAAE,uBAAuB,EAAE,MAAM,2BAA2B,CAAA;AACnE,YAAY,EAAE,8BAA8B,EAAE,MAAM,iDAAiD,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/content/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,8BAA8B,EAAE,MAAM,iDAAiD,CAAA;AACrG,OAAO,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAA;AAC7D,OAAO,EAAE,qBAAqB,EAAE,MAAM,yBAAyB,CAAA;AAC/D,OAAO,EAAE,qBAAqB,EAAE,MAAM,yBAAyB,CAAA;AAC/D,OAAO,EAAE,uBAAuB,EAAE,MAAM,2BAA2B,CAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"isMobile.d.ts","sourceRoot":"","sources":["../../../src/device/isMobile.ts"],"names":[],"mappings":"AAGA;;;GAGG;AACH,eAAO,MAAM,QAAQ,QAAO,OACe,CAAA"}
1
+ {"version":3,"file":"isMobile.d.ts","sourceRoot":"","sources":["../../../src/device/isMobile.ts"],"names":[],"mappings":"AAGA;;;GAGG;AACH,eAAO,MAAM,QAAQ,QAAO,OAAoD,CAAA"}
@@ -1,4 +1,4 @@
1
- export { useInAppLinkHandling } from './useInAppLinkHandling';
2
1
  export type { UseInAppLinkHandlingOptions } from '../../types/links/UseInAppLinkHandlingOptions';
3
2
  export type { UseInAppLinkHandlingReturn } from '../../types/links/UseInAppLinkHandlingReturn';
3
+ export { useInAppLinkHandling } from './useInAppLinkHandling';
4
4
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/links/react/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAA;AAC7D,YAAY,EAAE,2BAA2B,EAAE,MAAM,+CAA+C,CAAA;AAChG,YAAY,EAAE,0BAA0B,EAAE,MAAM,8CAA8C,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/links/react/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,2BAA2B,EAAE,MAAM,+CAA+C,CAAA;AAChG,YAAY,EAAE,0BAA0B,EAAE,MAAM,8CAA8C,CAAA;AAC9F,OAAO,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAA"}
@@ -0,0 +1,3 @@
1
+ export { injectShadowStyles } from './injectShadowStyles';
2
+ export { isShadowRoot } from './isShadowRoot';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/shadow/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAA;AACzD,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAA"}
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Injects a CSS string into a ShadowRoot as a `<style>` element.
3
+ *
4
+ * Idempotent: reuses the element with the given id and only updates it when the
5
+ * CSS changed. Throws when the target is not a ShadowRoot, so styles can never
6
+ * leak outside the shadow tree.
7
+ * @param {ShadowRoot} shadowRoot - The shadow root to inject into (required for isolation).
8
+ * @param {string} cssText - The CSS to inject.
9
+ * @param {string} styleElementId - Stable id for the injected `<style>` element.
10
+ * @returns {void} Nothing.
11
+ * @throws {Error} When `shadowRoot` is not a ShadowRoot instance.
12
+ */
13
+ export declare const injectShadowStyles: (shadowRoot: ShadowRoot, cssText: string, styleElementId: string) => void;
14
+ //# sourceMappingURL=injectShadowStyles.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"injectShadowStyles.d.ts","sourceRoot":"","sources":["../../../src/shadow/injectShadowStyles.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,kBAAkB,GAC7B,YAAY,UAAU,EACtB,SAAS,MAAM,EACf,gBAAgB,MAAM,KACrB,IAoBF,CAAA"}
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Type guard for a real ShadowRoot, used to ensure style injection never targets
3
+ * `document.head`/`document.body` (which would break Shadow DOM style isolation).
4
+ * @param {unknown} value - The candidate value.
5
+ * @returns {boolean} True when the value is a ShadowRoot.
6
+ */
7
+ export declare const isShadowRoot: (value: unknown) => value is ShadowRoot;
8
+ //# sourceMappingURL=isShadowRoot.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"isShadowRoot.d.ts","sourceRoot":"","sources":["../../../src/shadow/isShadowRoot.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,eAAO,MAAM,YAAY,GAAI,OAAO,OAAO,KAAG,KAAK,IAAI,UAStD,CAAA"}
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Holds the DOM element portalled components should render into, so portals stay
3
+ * inside the widget's ShadowRoot (preserving style isolation).
4
+ */
5
+ export declare const PortalContainerContext: import('react').Context<HTMLElement | null>;
6
+ //# sourceMappingURL=PortalContainerContext.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PortalContainerContext.d.ts","sourceRoot":"","sources":["../../../../src/shadow/react/PortalContainerContext.ts"],"names":[],"mappings":"AAEA;;;GAGG;AACH,eAAO,MAAM,sBAAsB,6CAA0C,CAAA"}
@@ -0,0 +1,10 @@
1
+ import { ReactElement } from 'react';
2
+ import { PortalContainerProviderProps } from '../../types/shadow/PortalContainerProviderProps';
3
+ /**
4
+ * Provides the portal container element to descendants. Built with createElement
5
+ * (no JSX) so the library has no JSX-runtime build coupling.
6
+ * @param {PortalContainerProviderProps} props - The portal container and children.
7
+ * @returns {ReactElement} The provider element wrapping the children.
8
+ */
9
+ export declare const PortalContainerProvider: ({ portalContainer, children, }: PortalContainerProviderProps) => ReactElement;
10
+ //# sourceMappingURL=PortalContainerProvider.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PortalContainerProvider.d.ts","sourceRoot":"","sources":["../../../../src/shadow/react/PortalContainerProvider.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,OAAO,CAAA;AAGzC,OAAO,KAAK,EAAE,4BAA4B,EAAE,MAAM,iDAAiD,CAAA;AAGnG;;;;;GAKG;AACH,eAAO,MAAM,uBAAuB,GAAI,gCAGrC,4BAA4B,KAAG,YAK/B,CAAA"}
@@ -0,0 +1,15 @@
1
+ import { EnsureShadowMountOptions } from '../../types/shadow/EnsureShadowMountOptions';
2
+ import { ShadowMount } from '../../types/shadow/ShadowMount';
3
+ /**
4
+ * Ensures a ShadowRoot mount exists on the host element for a widget.
5
+ *
6
+ * Creates stable React-mount and portal-container nodes, injects the widget's CSS
7
+ * into the shadow root only (never into the document), and configures an Emotion
8
+ * cache scoped to the shadow root. All internal ids are namespaced by `cacheKey`
9
+ * so editor and runtime mounts on the same host never collide.
10
+ * @param {HTMLElement} host - The host element to attach the shadow root to.
11
+ * @param {EnsureShadowMountOptions} options - The cache key and the widget's CSS text.
12
+ * @returns {ShadowMount} The shadow root, mount nodes and the scoped Emotion cache.
13
+ */
14
+ export declare const ensureShadowMount: (host: HTMLElement, { cacheKey, cssText }: EnsureShadowMountOptions) => ShadowMount;
15
+ //# sourceMappingURL=ensureShadowMount.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ensureShadowMount.d.ts","sourceRoot":"","sources":["../../../../src/shadow/react/ensureShadowMount.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,6CAA6C,CAAA;AAC3F,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,gCAAgC,CAAA;AAUjE;;;;;;;;;;GAUG;AACH,eAAO,MAAM,iBAAiB,GAC5B,MAAM,WAAW,EACjB,uBAAuB,wBAAwB,KAC9C,WA2BF,CAAA"}
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Returns the shadow-root child `<div>` with the given id, creating it if absent.
3
+ * @param {ShadowRoot} root - The shadow root to search/append within.
4
+ * @param {string} id - The element id.
5
+ * @returns {HTMLDivElement} The existing or newly created div.
6
+ */
7
+ export declare const getOrCreateDiv: (root: ShadowRoot, id: string) => HTMLDivElement;
8
+ //# sourceMappingURL=getOrCreateDiv.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"getOrCreateDiv.d.ts","sourceRoot":"","sources":["../../../../src/shadow/react/getOrCreateDiv.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,eAAO,MAAM,cAAc,GACzB,MAAM,UAAU,EAChB,IAAI,MAAM,KACT,cAQF,CAAA"}
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Returns the shadow-root `<meta name="emotion-insertion-point">` with the given
3
+ * id, creating it if absent. Keeps Emotion's styles ordered before shadow content.
4
+ * @param {ShadowRoot} root - The shadow root to search/append within.
5
+ * @param {string} id - The element id.
6
+ * @returns {HTMLMetaElement} The existing or newly created insertion-point meta.
7
+ */
8
+ export declare const getOrCreateEmotionInsertionPoint: (root: ShadowRoot, id: string) => HTMLMetaElement;
9
+ //# sourceMappingURL=getOrCreateEmotionInsertionPoint.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"getOrCreateEmotionInsertionPoint.d.ts","sourceRoot":"","sources":["../../../../src/shadow/react/getOrCreateEmotionInsertionPoint.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,eAAO,MAAM,gCAAgC,GAC3C,MAAM,UAAU,EAChB,IAAI,MAAM,KACT,eASF,CAAA"}
@@ -0,0 +1,7 @@
1
+ export type { EnsureShadowMountOptions } from '../../types/shadow/EnsureShadowMountOptions';
2
+ export type { PortalContainerProviderProps } from '../../types/shadow/PortalContainerProviderProps';
3
+ export type { ShadowMount } from '../../types/shadow/ShadowMount';
4
+ export { ensureShadowMount } from './ensureShadowMount';
5
+ export { PortalContainerProvider } from './PortalContainerProvider';
6
+ export { usePortalContainer } from './usePortalContainer';
7
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/shadow/react/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,wBAAwB,EAAE,MAAM,6CAA6C,CAAA;AAC3F,YAAY,EAAE,4BAA4B,EAAE,MAAM,iDAAiD,CAAA;AACnG,YAAY,EAAE,WAAW,EAAE,MAAM,gCAAgC,CAAA;AACjE,OAAO,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAA;AACvD,OAAO,EAAE,uBAAuB,EAAE,MAAM,2BAA2B,CAAA;AACnE,OAAO,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAA"}
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Returns the portal container element used to keep portals inside the ShadowRoot.
3
+ * @returns {HTMLElement | null} The portal container, or null when not provided.
4
+ */
5
+ export declare const usePortalContainer: () => HTMLElement | null;
6
+ //# sourceMappingURL=usePortalContainer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"usePortalContainer.d.ts","sourceRoot":"","sources":["../../../../src/shadow/react/usePortalContainer.ts"],"names":[],"mappings":"AAIA;;;GAGG;AACH,eAAO,MAAM,kBAAkB,QAAO,WAAW,GAAG,IAEnD,CAAA"}
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Options for ensureShadowMount.
3
+ */
4
+ export interface EnsureShadowMountOptions {
5
+ /**
6
+ * Stable, unique key. Namespaces the mount's internal element ids (so an editor
7
+ * and a runtime mount on the same host never collide) and is used as the
8
+ * Emotion cache key.
9
+ */
10
+ cacheKey: string;
11
+ /** The widget's shadow CSS text, injected into the shadow root. */
12
+ cssText: string;
13
+ }
14
+ //# sourceMappingURL=EnsureShadowMountOptions.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"EnsureShadowMountOptions.d.ts","sourceRoot":"","sources":["../../../../src/types/shadow/EnsureShadowMountOptions.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,MAAM,WAAW,wBAAwB;IACvC;;;;OAIG;IACH,QAAQ,EAAE,MAAM,CAAA;IAChB,mEAAmE;IACnE,OAAO,EAAE,MAAM,CAAA;CAChB"}
@@ -0,0 +1,10 @@
1
+ import { ReactNode } from 'react';
2
+ /**
3
+ * Props for PortalContainerProvider.
4
+ */
5
+ export interface PortalContainerProviderProps {
6
+ /** The DOM element portalled content should render into (inside the shadow root). */
7
+ portalContainer: HTMLElement;
8
+ children: ReactNode;
9
+ }
10
+ //# sourceMappingURL=PortalContainerProviderProps.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PortalContainerProviderProps.d.ts","sourceRoot":"","sources":["../../../../src/types/shadow/PortalContainerProviderProps.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAEtC;;GAEG;AACH,MAAM,WAAW,4BAA4B;IAC3C,qFAAqF;IACrF,eAAe,EAAE,WAAW,CAAA;IAC5B,QAAQ,EAAE,SAAS,CAAA;CACpB"}
@@ -0,0 +1,13 @@
1
+ import { EmotionCache } from '@emotion/cache';
2
+ /**
3
+ * The shadow-root mount a widget renders into: the root itself, the React mount
4
+ * node, the portal container node (kept inside the shadow tree for isolation),
5
+ * and the Emotion cache scoped to the shadow root.
6
+ */
7
+ export interface ShadowMount {
8
+ shadowRoot: ShadowRoot;
9
+ reactMountEl: HTMLDivElement;
10
+ portalContainerEl: HTMLDivElement;
11
+ emotionCache: EmotionCache;
12
+ }
13
+ //# sourceMappingURL=ShadowMount.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ShadowMount.d.ts","sourceRoot":"","sources":["../../../../src/types/shadow/ShadowMount.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAA;AAElD;;;;GAIG;AACH,MAAM,WAAW,WAAW;IAC1B,UAAU,EAAE,UAAU,CAAA;IACtB,YAAY,EAAE,cAAc,CAAA;IAC5B,iBAAiB,EAAE,cAAc,CAAA;IACjC,YAAY,EAAE,YAAY,CAAA;CAC3B"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@favish/staffbase-utils",
3
- "version": "0.13.0",
3
+ "version": "0.14.0",
4
4
  "description": "Shared internal/host utilities for Staffbase widgets",
5
5
  "author": "Favish <dev@favish.com>",
6
6
  "license": "UNLICENSED",
@@ -53,6 +53,16 @@
53
53
  "import": "./dist/links/react.es.mjs",
54
54
  "require": "./dist/links/react.cjs.js"
55
55
  },
56
+ "./shadow": {
57
+ "types": "./dist/src/shadow/index.d.ts",
58
+ "import": "./dist/shadow.es.mjs",
59
+ "require": "./dist/shadow.cjs.js"
60
+ },
61
+ "./shadow/react": {
62
+ "types": "./dist/src/shadow/react/index.d.ts",
63
+ "import": "./dist/shadow/react.es.mjs",
64
+ "require": "./dist/shadow/react.cjs.js"
65
+ },
56
66
  "./widgets/react": {
57
67
  "types": "./dist/src/widgets/react/index.d.ts",
58
68
  "import": "./dist/widgets/react.es.mjs",
@@ -73,10 +83,14 @@
73
83
  "access": "public"
74
84
  },
75
85
  "peerDependencies": {
86
+ "@emotion/cache": "^11.0.0",
76
87
  "react": "^18.0.0 || ^19.0.0",
77
88
  "react-dom": "^18.0.0 || ^19.0.0"
78
89
  },
79
90
  "peerDependenciesMeta": {
91
+ "@emotion/cache": {
92
+ "optional": true
93
+ },
80
94
  "react": {
81
95
  "optional": true
82
96
  },
@@ -85,6 +99,7 @@
85
99
  }
86
100
  },
87
101
  "devDependencies": {
102
+ "@emotion/cache": "11.14.0",
88
103
  "@eslint/eslintrc": "^3.3.5",
89
104
  "@eslint/js": "^10.0.1",
90
105
  "@testing-library/jest-dom": "^6.9.1",