@tribepad/themis 1.6.5 → 1.6.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +9 -0
- package/dist/elements/AlertDialog/index.js +1 -1
- package/dist/elements/AlertDialog/index.js.map +1 -1
- package/dist/elements/AlertDialog/index.mjs +1 -1
- package/dist/elements/AlertDialog/index.mjs.map +1 -1
- package/dist/elements/Breadcrumbs/index.js +1 -1
- package/dist/elements/Breadcrumbs/index.js.map +1 -1
- package/dist/elements/Breadcrumbs/index.mjs +1 -1
- package/dist/elements/Breadcrumbs/index.mjs.map +1 -1
- package/dist/elements/Combobox/Combobox.d.ts.map +1 -1
- package/dist/elements/Combobox/index.js +1 -1
- package/dist/elements/Combobox/index.js.map +1 -1
- package/dist/elements/Combobox/index.mjs +1 -1
- package/dist/elements/Combobox/index.mjs.map +1 -1
- package/dist/elements/DatePicker/DatePicker.d.ts.map +1 -1
- package/dist/elements/DatePicker/DateRangePicker.d.ts.map +1 -1
- package/dist/elements/DatePicker/index.js +1 -1
- package/dist/elements/DatePicker/index.js.map +1 -1
- package/dist/elements/DatePicker/index.mjs +1 -1
- package/dist/elements/DatePicker/index.mjs.map +1 -1
- package/dist/elements/Dropdown/Dropdown.d.ts.map +1 -1
- package/dist/elements/Dropdown/index.js +1 -1
- package/dist/elements/Dropdown/index.js.map +1 -1
- package/dist/elements/Dropdown/index.mjs +1 -1
- package/dist/elements/Dropdown/index.mjs.map +1 -1
- package/dist/elements/Filters/FilterPopover.d.ts.map +1 -1
- package/dist/elements/Filters/index.js +1 -1
- package/dist/elements/Filters/index.js.map +1 -1
- package/dist/elements/Filters/index.mjs +1 -1
- package/dist/elements/Filters/index.mjs.map +1 -1
- package/dist/elements/Modal/Modal.d.ts +1 -1
- package/dist/elements/Modal/Modal.d.ts.map +1 -1
- package/dist/elements/Modal/Modal.types.d.ts +2 -0
- package/dist/elements/Modal/Modal.types.d.ts.map +1 -1
- package/dist/elements/Modal/index.js +1 -1
- package/dist/elements/Modal/index.js.map +1 -1
- package/dist/elements/Modal/index.mjs +1 -1
- package/dist/elements/Modal/index.mjs.map +1 -1
- package/dist/elements/PageTitle/index.js +1 -1
- package/dist/elements/PageTitle/index.js.map +1 -1
- package/dist/elements/PageTitle/index.mjs +1 -1
- package/dist/elements/PageTitle/index.mjs.map +1 -1
- package/dist/elements/Panel/Panel.d.ts.map +1 -1
- package/dist/elements/Panel/index.js +1 -1
- package/dist/elements/Panel/index.js.map +1 -1
- package/dist/elements/Panel/index.mjs +1 -1
- package/dist/elements/Panel/index.mjs.map +1 -1
- package/dist/elements/Resizable/components/ResizablePopover.d.ts.map +1 -1
- package/dist/elements/Resizable/index.js +1 -1
- package/dist/elements/Resizable/index.js.map +1 -1
- package/dist/elements/Resizable/index.mjs +1 -1
- package/dist/elements/Resizable/index.mjs.map +1 -1
- package/dist/elements/Select/Select.d.ts.map +1 -1
- package/dist/elements/Select/index.js +1 -1
- package/dist/elements/Select/index.js.map +1 -1
- package/dist/elements/Select/index.mjs +1 -1
- package/dist/elements/Select/index.mjs.map +1 -1
- package/dist/elements/Sidebar/SidebarMobileDrawer.d.ts.map +1 -1
- package/dist/elements/Sidebar/index.js +1 -1
- package/dist/elements/Sidebar/index.js.map +1 -1
- package/dist/elements/Sidebar/index.mjs +1 -1
- package/dist/elements/Sidebar/index.mjs.map +1 -1
- package/dist/elements/Table/index.js +1 -1
- package/dist/elements/Table/index.js.map +1 -1
- package/dist/elements/Table/index.mjs +1 -1
- package/dist/elements/Table/index.mjs.map +1 -1
- package/dist/elements/Wizard/index.js +1 -1
- package/dist/elements/Wizard/index.js.map +1 -1
- package/dist/elements/Wizard/index.mjs +1 -1
- package/dist/elements/Wizard/index.mjs.map +1 -1
- package/dist/elements/index.js +1 -1
- package/dist/elements/index.js.map +1 -1
- package/dist/elements/index.mjs +1 -1
- package/dist/elements/index.mjs.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +2 -2
- package/dist/index.mjs.map +1 -1
- package/dist/utils/browserExtensionOverlay.d.ts +36 -0
- package/dist/utils/browserExtensionOverlay.d.ts.map +1 -0
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +1 -1
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/index.mjs +1 -1
- package/dist/utils/index.mjs.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detection + dismissal guard for in-page writing-assistant browser extensions
|
|
3
|
+
* (Grammarly, LanguageTool, Wordtune, …).
|
|
4
|
+
*
|
|
5
|
+
* These extensions inject their suggestion UI as elements appended to `<body>`,
|
|
6
|
+
* outside any open overlay's DOM subtree. React Aria's `isDismissable` behaviour
|
|
7
|
+
* treats a click on that injected UI as an "outside click" and dismisses the
|
|
8
|
+
* overlay — so clicking a spelling suggestion silently closes the form pop-up
|
|
9
|
+
* and discards the user's in-progress correction.
|
|
10
|
+
*
|
|
11
|
+
* The guard below lets overlays keep open when the interaction lands on an
|
|
12
|
+
* extension-injected node, while every other outside click, Escape, and the
|
|
13
|
+
* close button behave exactly as before.
|
|
14
|
+
*/
|
|
15
|
+
/**
|
|
16
|
+
* Returns `true` when `element` belongs to an injected in-page writing
|
|
17
|
+
* assistant (Grammarly, LanguageTool, Wordtune, …).
|
|
18
|
+
*
|
|
19
|
+
* UX-ONLY heuristic. This is a deliberately spoofable DOM sniff used solely to
|
|
20
|
+
* keep a form pop-up open when a writing-assistant suggestion is clicked. It is
|
|
21
|
+
* NOT a security/authorization control and must never gate access to data or
|
|
22
|
+
* actions (the frontend is not a security boundary). Pure function — never log
|
|
23
|
+
* the element (it can reference arbitrary DOM / user content).
|
|
24
|
+
*/
|
|
25
|
+
export declare function isExtensionInjectedElement(element: Element): boolean;
|
|
26
|
+
/**
|
|
27
|
+
* Builds a `shouldCloseOnInteractOutside` predicate for React Aria overlays
|
|
28
|
+
* that keeps the overlay open when the interaction targets an extension-injected
|
|
29
|
+
* element, while preserving any consumer-supplied predicate.
|
|
30
|
+
*
|
|
31
|
+
* React Aria semantics: the predicate returns `true` ⇒ the overlay should
|
|
32
|
+
* close. So we close only when the element is NOT extension-injected AND the
|
|
33
|
+
* consumer (if any) also agrees it should close.
|
|
34
|
+
*/
|
|
35
|
+
export declare function createInteractOutsideGuard(consumerShouldClose?: (element: Element) => boolean): (element: Element) => boolean;
|
|
36
|
+
//# sourceMappingURL=browserExtensionOverlay.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"browserExtensionOverlay.d.ts","sourceRoot":"","sources":["../../src/utils/browserExtensionOverlay.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAIH;;;;;;;;;GASG;AACH,wBAAgB,0BAA0B,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAwCpE;AAED;;;;;;;;GAQG;AACH,wBAAgB,0BAA0B,CACxC,mBAAmB,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,OAAO,GAClD,CAAC,OAAO,EAAE,OAAO,KAAK,OAAO,CAG/B"}
|
package/dist/utils/index.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,EAAE,EAAE,MAAM,MAAM,CAAC;AAC1B,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,EAAE,EAAE,MAAM,MAAM,CAAC;AAC1B,OAAO,EACL,0BAA0B,EAC1B,0BAA0B,GAC3B,MAAM,2BAA2B,CAAC;AACnC,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC"}
|
package/dist/utils/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
'use strict';var clsx=require('clsx'),tailwindMerge=require('tailwind-merge');function
|
|
1
|
+
'use strict';var clsx=require('clsx'),tailwindMerge=require('tailwind-merge');function i(...t){return tailwindMerge.twMerge(clsx.clsx(t))}function u(t){if(!(t instanceof Element))return false;let e=t;for(let o=0;e&&o<100;o++){let a=e.tagName?.toLowerCase()??"";if(a.startsWith("grammarly-")||a.startsWith("lt-")||e.hasAttribute("data-grammarly-shadow-root"))return true;if(e.parentElement)e=e.parentElement;else {let l=e.getRootNode();e=l instanceof ShadowRoot?l.host:null;}}let n=t;for(;n&&!n.tagName.includes("-");)n=n.parentElement;if(!n)return false;let r=n.parentElement;return r===document.body||r===document.documentElement}function f(t){return e=>!u(e)&&(t?.(e)??true)}function c(t,e){return !(t===false||e)}exports.cn=i;exports.createInteractOutsideGuard=f;exports.isExtensionInjectedElement=u;exports.shouldAnimate=c;//# sourceMappingURL=index.js.map
|
|
2
2
|
//# sourceMappingURL=index.js.map
|
package/dist/utils/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/utils/cn.ts","../../src/utils/shouldAnimate.ts"],"names":["cn","inputs","twMerge","clsx","shouldAnimate","animated","prefersReducedMotion"],"mappings":"8EAcO,SAASA,CAAAA,CAAAA,GAAMC,CAAAA,CAA8B,CAClD,OAAOC,
|
|
1
|
+
{"version":3,"sources":["../../src/utils/cn.ts","../../src/utils/browserExtensionOverlay.ts","../../src/utils/shouldAnimate.ts"],"names":["cn","inputs","twMerge","clsx","isExtensionInjectedElement","element","node","depth","tag","root","custom","parent","createInteractOutsideGuard","consumerShouldClose","shouldAnimate","animated","prefersReducedMotion"],"mappings":"8EAcO,SAASA,CAAAA,CAAAA,GAAMC,CAAAA,CAA8B,CAClD,OAAOC,sBAAQC,SAAAA,CAAKF,CAAM,CAAC,CAC7B,CCWO,SAASG,CAAAA,CAA2BC,CAAAA,CAA2B,CACpE,GAAI,EAAEA,CAAAA,YAAmB,OAAA,CAAA,CAAU,OAAO,MAAA,CAU1C,IAAIC,CAAAA,CAAuBD,EAC3B,IAAA,IAASE,CAAAA,CAAQ,CAAA,CAAGD,CAAAA,EAAQC,CAAAA,CAAQ,GAAA,CAAoBA,CAAAA,EAAAA,CAAS,CAC/D,IAAMC,CAAAA,CAAMF,CAAAA,CAAK,OAAA,EAAS,WAAA,EAAY,EAAK,EAAA,CAG3C,GAFIE,EAAI,UAAA,CAAW,YAAY,CAAA,EAC3BA,CAAAA,CAAI,UAAA,CAAW,KAAK,CAAA,EACpBF,CAAAA,CAAK,aAAa,4BAA4B,CAAA,CAAG,OAAO,KAAA,CAE5D,GAAIA,CAAAA,CAAK,aAAA,CACPA,CAAAA,CAAOA,EAAK,aAAA,CAAA,KACP,CACL,IAAMG,CAAAA,CAAOH,CAAAA,CAAK,WAAA,EAAY,CAC9BA,CAAAA,CAAOG,CAAAA,YAAgB,UAAA,CAAaA,CAAAA,CAAK,IAAA,CAAO,KAClD,CACF,CAQA,IAAIC,EAAyBL,CAAAA,CAC7B,KAAOK,CAAAA,EAAU,CAACA,CAAAA,CAAO,OAAA,CAAQ,QAAA,CAAS,GAAG,GAC3CA,CAAAA,CAASA,CAAAA,CAAO,aAAA,CAElB,GAAI,CAACA,CAAAA,CAAQ,OAAO,MAAA,CAEpB,IAAMC,CAAAA,CAASD,CAAAA,CAAO,aAAA,CACtB,OAAOC,CAAAA,GAAW,QAAA,CAAS,IAAA,EAAQA,CAAAA,GAAW,QAAA,CAAS,eACzD,CAWO,SAASC,CAAAA,CACdC,CAAAA,CAC+B,CAC/B,OAAQR,GACN,CAACD,CAAAA,CAA2BC,CAAO,CAAA,GAAMQ,CAAAA,GAAsBR,CAAO,CAAA,EAAK,IAAA,CAC/E,CCzEO,SAASS,CAAAA,CACdC,CAAAA,CACAC,CAAAA,CACS,CAET,OADI,EAAAD,CAAAA,GAAa,OACbC,CAAAA,CAEN","file":"index.js","sourcesContent":["/**\n * Class Name Utility\n * Merges Tailwind CSS classes with conflict resolution\n *\n * Combines clsx for conditional classes and tailwind-merge for deduplication\n *\n * @example\n * cn('px-2 py-1', 'px-4') // => 'py-1 px-4' (px-4 overrides px-2)\n * cn('text-red-500', condition && 'text-blue-500') // => conditional application\n */\n\nimport { clsx, type ClassValue } from 'clsx';\nimport { twMerge } from 'tailwind-merge';\n\nexport function cn(...inputs: ClassValue[]): string {\n return twMerge(clsx(inputs));\n}\n","/**\n * Detection + dismissal guard for in-page writing-assistant browser extensions\n * (Grammarly, LanguageTool, Wordtune, …).\n *\n * These extensions inject their suggestion UI as elements appended to `<body>`,\n * outside any open overlay's DOM subtree. React Aria's `isDismissable` behaviour\n * treats a click on that injected UI as an \"outside click\" and dismisses the\n * overlay — so clicking a spelling suggestion silently closes the form pop-up\n * and discards the user's in-progress correction.\n *\n * The guard below lets overlays keep open when the interaction lands on an\n * extension-injected node, while every other outside click, Escape, and the\n * close button behave exactly as before.\n */\n\nconst MAX_ANCESTOR_DEPTH = 100;\n\n/**\n * Returns `true` when `element` belongs to an injected in-page writing\n * assistant (Grammarly, LanguageTool, Wordtune, …).\n *\n * UX-ONLY heuristic. This is a deliberately spoofable DOM sniff used solely to\n * keep a form pop-up open when a writing-assistant suggestion is clicked. It is\n * NOT a security/authorization control and must never gate access to data or\n * actions (the frontend is not a security boundary). Pure function — never log\n * the element (it can reference arbitrary DOM / user content).\n */\nexport function isExtensionInjectedElement(element: Element): boolean {\n if (!(element instanceof Element)) return false;\n\n // 1. Known-vendor markers (authoritative). Walk up via parentElement, and\n // climb shadow boundaries via getRootNode().host so detection still finds\n // the <grammarly-extension> host if React Aria's shadowDOM flag is ever\n // enabled (it defaults to false today → the document-level listener\n // already receives the retargeted host, so the parentElement walk alone\n // suffices currently). Depth-bounded as a defensive guard against a\n // crafted/overridden getRootNode/host returning a cycle (which would\n // otherwise hang the tab — a client-side DoS).\n let node: Element | null = element;\n for (let depth = 0; node && depth < MAX_ANCESTOR_DEPTH; depth++) {\n const tag = node.tagName?.toLowerCase() ?? '';\n if (tag.startsWith('grammarly-')) return true; // Grammarly\n if (tag.startsWith('lt-')) return true; // LanguageTool\n if (node.hasAttribute('data-grammarly-shadow-root')) return true;\n\n if (node.parentElement) {\n node = node.parentElement;\n } else {\n const root = node.getRootNode();\n node = root instanceof ShadowRoot ? root.host : null;\n }\n }\n\n // 2. Structural fallback (best-effort, future-proofs the long tail).\n // Extensions inject their UI as a custom element (hyphenated tag name)\n // mounted as a top-level sibling of the React root — i.e. a direct child\n // of <body>/<html>. Our app never renders body-level custom elements, and\n // React Aria portals render plain <div>s, so this stays safe for any\n // future first-party web component nested inside the app tree.\n let custom: Element | null = element;\n while (custom && !custom.tagName.includes('-')) {\n custom = custom.parentElement;\n }\n if (!custom) return false;\n\n const parent = custom.parentElement;\n return parent === document.body || parent === document.documentElement;\n}\n\n/**\n * Builds a `shouldCloseOnInteractOutside` predicate for React Aria overlays\n * that keeps the overlay open when the interaction targets an extension-injected\n * element, while preserving any consumer-supplied predicate.\n *\n * React Aria semantics: the predicate returns `true` ⇒ the overlay should\n * close. So we close only when the element is NOT extension-injected AND the\n * consumer (if any) also agrees it should close.\n */\nexport function createInteractOutsideGuard(\n consumerShouldClose?: (element: Element) => boolean,\n): (element: Element) => boolean {\n return (element: Element): boolean =>\n !isExtensionInjectedElement(element) && (consumerShouldClose?.(element) ?? true);\n}\n","/**\n * Determines whether a component should animate.\n *\n * Returns `false` if:\n * - The `animated` prop is explicitly `false`\n * - The user prefers reduced motion\n *\n * @param animated - The component's `animated` prop value (default: `true`)\n * @param prefersReducedMotion - Whether the user prefers reduced motion\n */\nexport function shouldAnimate(\n animated: boolean | undefined,\n prefersReducedMotion: boolean,\n): boolean {\n if (animated === false) return false;\n if (prefersReducedMotion) return false;\n return true;\n}\n"]}
|
package/dist/utils/index.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import {clsx}from'clsx';import {twMerge}from'tailwind-merge';function
|
|
1
|
+
import {clsx}from'clsx';import {twMerge}from'tailwind-merge';function i(...t){return twMerge(clsx(t))}function u(t){if(!(t instanceof Element))return false;let e=t;for(let o=0;e&&o<100;o++){let a=e.tagName?.toLowerCase()??"";if(a.startsWith("grammarly-")||a.startsWith("lt-")||e.hasAttribute("data-grammarly-shadow-root"))return true;if(e.parentElement)e=e.parentElement;else {let l=e.getRootNode();e=l instanceof ShadowRoot?l.host:null;}}let n=t;for(;n&&!n.tagName.includes("-");)n=n.parentElement;if(!n)return false;let r=n.parentElement;return r===document.body||r===document.documentElement}function f(t){return e=>!u(e)&&(t?.(e)??true)}function c(t,e){return !(t===false||e)}export{i as cn,f as createInteractOutsideGuard,u as isExtensionInjectedElement,c as shouldAnimate};//# sourceMappingURL=index.mjs.map
|
|
2
2
|
//# sourceMappingURL=index.mjs.map
|
package/dist/utils/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/utils/cn.ts","../../src/utils/shouldAnimate.ts"],"names":["cn","inputs","twMerge","clsx","shouldAnimate","animated","prefersReducedMotion"],"mappings":"6DAcO,SAASA,CAAAA,CAAAA,GAAMC,CAAAA,CAA8B,CAClD,OAAOC,
|
|
1
|
+
{"version":3,"sources":["../../src/utils/cn.ts","../../src/utils/browserExtensionOverlay.ts","../../src/utils/shouldAnimate.ts"],"names":["cn","inputs","twMerge","clsx","isExtensionInjectedElement","element","node","depth","tag","root","custom","parent","createInteractOutsideGuard","consumerShouldClose","shouldAnimate","animated","prefersReducedMotion"],"mappings":"6DAcO,SAASA,CAAAA,CAAAA,GAAMC,CAAAA,CAA8B,CAClD,OAAOC,QAAQC,IAAAA,CAAKF,CAAM,CAAC,CAC7B,CCWO,SAASG,CAAAA,CAA2BC,CAAAA,CAA2B,CACpE,GAAI,EAAEA,CAAAA,YAAmB,OAAA,CAAA,CAAU,OAAO,MAAA,CAU1C,IAAIC,CAAAA,CAAuBD,EAC3B,IAAA,IAASE,CAAAA,CAAQ,CAAA,CAAGD,CAAAA,EAAQC,CAAAA,CAAQ,GAAA,CAAoBA,CAAAA,EAAAA,CAAS,CAC/D,IAAMC,CAAAA,CAAMF,CAAAA,CAAK,OAAA,EAAS,WAAA,EAAY,EAAK,EAAA,CAG3C,GAFIE,EAAI,UAAA,CAAW,YAAY,CAAA,EAC3BA,CAAAA,CAAI,UAAA,CAAW,KAAK,CAAA,EACpBF,CAAAA,CAAK,aAAa,4BAA4B,CAAA,CAAG,OAAO,KAAA,CAE5D,GAAIA,CAAAA,CAAK,aAAA,CACPA,CAAAA,CAAOA,EAAK,aAAA,CAAA,KACP,CACL,IAAMG,CAAAA,CAAOH,CAAAA,CAAK,WAAA,EAAY,CAC9BA,CAAAA,CAAOG,CAAAA,YAAgB,UAAA,CAAaA,CAAAA,CAAK,IAAA,CAAO,KAClD,CACF,CAQA,IAAIC,EAAyBL,CAAAA,CAC7B,KAAOK,CAAAA,EAAU,CAACA,CAAAA,CAAO,OAAA,CAAQ,QAAA,CAAS,GAAG,GAC3CA,CAAAA,CAASA,CAAAA,CAAO,aAAA,CAElB,GAAI,CAACA,CAAAA,CAAQ,OAAO,MAAA,CAEpB,IAAMC,CAAAA,CAASD,CAAAA,CAAO,aAAA,CACtB,OAAOC,CAAAA,GAAW,QAAA,CAAS,IAAA,EAAQA,CAAAA,GAAW,QAAA,CAAS,eACzD,CAWO,SAASC,CAAAA,CACdC,CAAAA,CAC+B,CAC/B,OAAQR,GACN,CAACD,CAAAA,CAA2BC,CAAO,CAAA,GAAMQ,CAAAA,GAAsBR,CAAO,CAAA,EAAK,IAAA,CAC/E,CCzEO,SAASS,CAAAA,CACdC,CAAAA,CACAC,CAAAA,CACS,CAET,OADI,EAAAD,CAAAA,GAAa,OACbC,CAAAA,CAEN","file":"index.mjs","sourcesContent":["/**\n * Class Name Utility\n * Merges Tailwind CSS classes with conflict resolution\n *\n * Combines clsx for conditional classes and tailwind-merge for deduplication\n *\n * @example\n * cn('px-2 py-1', 'px-4') // => 'py-1 px-4' (px-4 overrides px-2)\n * cn('text-red-500', condition && 'text-blue-500') // => conditional application\n */\n\nimport { clsx, type ClassValue } from 'clsx';\nimport { twMerge } from 'tailwind-merge';\n\nexport function cn(...inputs: ClassValue[]): string {\n return twMerge(clsx(inputs));\n}\n","/**\n * Detection + dismissal guard for in-page writing-assistant browser extensions\n * (Grammarly, LanguageTool, Wordtune, …).\n *\n * These extensions inject their suggestion UI as elements appended to `<body>`,\n * outside any open overlay's DOM subtree. React Aria's `isDismissable` behaviour\n * treats a click on that injected UI as an \"outside click\" and dismisses the\n * overlay — so clicking a spelling suggestion silently closes the form pop-up\n * and discards the user's in-progress correction.\n *\n * The guard below lets overlays keep open when the interaction lands on an\n * extension-injected node, while every other outside click, Escape, and the\n * close button behave exactly as before.\n */\n\nconst MAX_ANCESTOR_DEPTH = 100;\n\n/**\n * Returns `true` when `element` belongs to an injected in-page writing\n * assistant (Grammarly, LanguageTool, Wordtune, …).\n *\n * UX-ONLY heuristic. This is a deliberately spoofable DOM sniff used solely to\n * keep a form pop-up open when a writing-assistant suggestion is clicked. It is\n * NOT a security/authorization control and must never gate access to data or\n * actions (the frontend is not a security boundary). Pure function — never log\n * the element (it can reference arbitrary DOM / user content).\n */\nexport function isExtensionInjectedElement(element: Element): boolean {\n if (!(element instanceof Element)) return false;\n\n // 1. Known-vendor markers (authoritative). Walk up via parentElement, and\n // climb shadow boundaries via getRootNode().host so detection still finds\n // the <grammarly-extension> host if React Aria's shadowDOM flag is ever\n // enabled (it defaults to false today → the document-level listener\n // already receives the retargeted host, so the parentElement walk alone\n // suffices currently). Depth-bounded as a defensive guard against a\n // crafted/overridden getRootNode/host returning a cycle (which would\n // otherwise hang the tab — a client-side DoS).\n let node: Element | null = element;\n for (let depth = 0; node && depth < MAX_ANCESTOR_DEPTH; depth++) {\n const tag = node.tagName?.toLowerCase() ?? '';\n if (tag.startsWith('grammarly-')) return true; // Grammarly\n if (tag.startsWith('lt-')) return true; // LanguageTool\n if (node.hasAttribute('data-grammarly-shadow-root')) return true;\n\n if (node.parentElement) {\n node = node.parentElement;\n } else {\n const root = node.getRootNode();\n node = root instanceof ShadowRoot ? root.host : null;\n }\n }\n\n // 2. Structural fallback (best-effort, future-proofs the long tail).\n // Extensions inject their UI as a custom element (hyphenated tag name)\n // mounted as a top-level sibling of the React root — i.e. a direct child\n // of <body>/<html>. Our app never renders body-level custom elements, and\n // React Aria portals render plain <div>s, so this stays safe for any\n // future first-party web component nested inside the app tree.\n let custom: Element | null = element;\n while (custom && !custom.tagName.includes('-')) {\n custom = custom.parentElement;\n }\n if (!custom) return false;\n\n const parent = custom.parentElement;\n return parent === document.body || parent === document.documentElement;\n}\n\n/**\n * Builds a `shouldCloseOnInteractOutside` predicate for React Aria overlays\n * that keeps the overlay open when the interaction targets an extension-injected\n * element, while preserving any consumer-supplied predicate.\n *\n * React Aria semantics: the predicate returns `true` ⇒ the overlay should\n * close. So we close only when the element is NOT extension-injected AND the\n * consumer (if any) also agrees it should close.\n */\nexport function createInteractOutsideGuard(\n consumerShouldClose?: (element: Element) => boolean,\n): (element: Element) => boolean {\n return (element: Element): boolean =>\n !isExtensionInjectedElement(element) && (consumerShouldClose?.(element) ?? true);\n}\n","/**\n * Determines whether a component should animate.\n *\n * Returns `false` if:\n * - The `animated` prop is explicitly `false`\n * - The user prefers reduced motion\n *\n * @param animated - The component's `animated` prop value (default: `true`)\n * @param prefersReducedMotion - Whether the user prefers reduced motion\n */\nexport function shouldAnimate(\n animated: boolean | undefined,\n prefersReducedMotion: boolean,\n): boolean {\n if (animated === false) return false;\n if (prefersReducedMotion) return false;\n return true;\n}\n"]}
|