@brightspot/ui 1.4.0 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) hide show
  1. package/README.md +12 -0
  2. package/dist/components/avatar/Avatar.d.ts +1 -1
  3. package/dist/components/avatar/Avatar.d.ts.map +1 -1
  4. package/dist/components/avatar/Avatar.js +3 -1
  5. package/dist/components/avatar/Avatar.js.map +1 -1
  6. package/dist/components/popover/Popover.d.ts +161 -0
  7. package/dist/components/popover/Popover.d.ts.map +1 -0
  8. package/dist/components/popover/Popover.js +436 -0
  9. package/dist/components/popover/Popover.js.map +1 -0
  10. package/dist/components/widget/Widget.css +104 -0
  11. package/dist/components/widget/Widget.d.ts +170 -0
  12. package/dist/components/widget/Widget.d.ts.map +1 -0
  13. package/dist/components/widget/Widget.js +434 -0
  14. package/dist/components/widget/Widget.js.map +1 -0
  15. package/dist/custom-elements.json +604 -101
  16. package/dist/global.d.ts +5 -0
  17. package/dist/storybook/assets/{Avatar.stories-DrhezTR1.js → Avatar.stories-CPVNxsaA.js} +37 -32
  18. package/dist/storybook/assets/AvatarGroup.stories-Bl65NGHl.js +225 -0
  19. package/dist/storybook/assets/{Badge.stories-DtJcBfOR.js → Badge.stories-Bbnc6fRy.js} +1 -1
  20. package/dist/storybook/assets/{Button.stories-BKUfLgSY.js → Button.stories-CRJ5n2y4.js} +1 -1
  21. package/dist/storybook/assets/{CircularProgress.stories-dpmD-XJT.js → CircularProgress.stories-D9vBj3JJ.js} +1 -1
  22. package/dist/storybook/assets/{ClipboardMixin.stories-C0pnQ7BY.js → ClipboardMixin.stories-Dm-Jm4yb.js} +7 -7
  23. package/dist/storybook/assets/Color-6BZIO3FS-BcNIJY1U.js +1 -0
  24. package/dist/storybook/assets/{Colors.stories-bKK25qgF.js → Colors.stories-B9_090wL.js} +1 -1
  25. package/dist/storybook/assets/ComponentStatesMixin-ChiFBCuo.js +1 -0
  26. package/dist/storybook/assets/{ComponentStatesMixin.stories-9mRp2zuB.js → ComponentStatesMixin.stories-DHv9MHmE.js} +3 -3
  27. package/dist/storybook/assets/{CopyToClipboard.stories-BW3oaT1i.js → CopyToClipboard.stories-gtJlTP1l.js} +1 -1
  28. package/dist/storybook/assets/{Debounce.stories-BXx3CKvQ.js → Debounce.stories-BBNX7mJA.js} +3 -3
  29. package/dist/storybook/assets/DocsRenderer-LL677BLK-D-E99pXl.js +758 -0
  30. package/dist/storybook/assets/{Events.stories-PBeiuWQn.js → Events.stories-DDmydlh_.js} +1 -1
  31. package/dist/storybook/assets/{Heading.stories-Djkl0MoC.js → Heading.stories-BLGfko-i.js} +1 -1
  32. package/dist/storybook/assets/{Icon.stories-Cam1fyud.js → Icon.stories-BHnAGcFF.js} +1 -1
  33. package/dist/storybook/assets/{LinearProgress.stories-BDNoYIJu.js → LinearProgress.stories-Dx26a0P_.js} +1 -1
  34. package/dist/storybook/assets/Popover.stories-CbqpY6YR.js +431 -0
  35. package/dist/storybook/assets/ReadyMixin-BHiHoIbr.js +1 -0
  36. package/dist/storybook/assets/{Rtc.stories-BrTAIAi1.js → Rtc.stories-CAjDv_Ub.js} +3 -3
  37. package/dist/storybook/assets/{ScrollShadow.stories-DHcKhkag.js → ScrollShadow.stories-BSV4U-tq.js} +1 -1
  38. package/dist/storybook/assets/{Throttle.stories-cSYT_BXu.js → Throttle.stories-kaxXQ8RZ.js} +8 -8
  39. package/dist/storybook/assets/Tooltip.stories-CsxXkztr.js +143 -0
  40. package/dist/storybook/assets/Widget.stories-DqATHnSq.js +233 -0
  41. package/dist/storybook/assets/WithTooltip-65CFNBJE-BtbbFYSA.js +9 -0
  42. package/dist/storybook/assets/custom-element-UsVr97OX.js +1 -0
  43. package/dist/storybook/assets/formatter-EIJCOSYU-C87Csnpu.js +1 -0
  44. package/dist/storybook/assets/if-defined-COHr0XBn.js +1 -0
  45. package/dist/storybook/assets/{iframe-BMxUFmpF.css → iframe-BkDGeDre.css} +1 -1
  46. package/dist/storybook/assets/iframe-CcloOV09.js +1061 -0
  47. package/dist/storybook/assets/index-DP7vnJf7.js +1 -0
  48. package/dist/storybook/assets/onFind-DqriYjEB.js +1 -0
  49. package/dist/storybook/assets/onFind.stories-BxvoC-Z-.js +1069 -0
  50. package/dist/storybook/assets/{onRemove.stories-C7W9KyRr.js → onRemove.stories-Dwoixzb0.js} +3 -3
  51. package/dist/storybook/assets/{onVisible.stories-CIl6R0q4.js → onVisible.stories-CinmRF9w.js} +10 -10
  52. package/dist/storybook/assets/syntaxhighlighter-ED5Y7EFY-BHLkDkOn.js +6 -0
  53. package/dist/storybook/iframe.html +57 -39
  54. package/dist/storybook/index.html +11 -4
  55. package/dist/storybook/index.json +1 -1
  56. package/dist/storybook/project.json +1 -1
  57. package/dist/storybook/sb-addons/docs-1/manager-bundle.js +1 -1
  58. package/dist/storybook/sb-addons/storybook-core-server-presets-0/common-manager-bundle.js +112 -290
  59. package/dist/storybook/sb-addons/vitest-2/manager-bundle.js +3 -0
  60. package/dist/storybook/sb-manager/globals-runtime.js +60754 -66346
  61. package/dist/storybook/sb-manager/globals.js +2 -3
  62. package/dist/storybook/sb-manager/manager-stores.js +23 -0
  63. package/dist/storybook/sb-manager/runtime.js +12983 -11699
  64. package/dist/storybook/vite-inject-mocker-entry.js +2 -2
  65. package/dist/tailwind-plugin-popover.d.ts +2 -0
  66. package/dist/tailwind-plugin-popover.d.ts.map +1 -0
  67. package/dist/tailwind-plugin-popover.js +177 -0
  68. package/dist/tailwind-plugin-popover.js.map +1 -0
  69. package/dist/tailwind-plugin-popover.ts +202 -0
  70. package/dist/tailwind-plugin-tooltip.d.ts +2 -0
  71. package/dist/tailwind-plugin-tooltip.d.ts.map +1 -0
  72. package/dist/tailwind-plugin-tooltip.js +184 -0
  73. package/dist/tailwind-plugin-tooltip.js.map +1 -0
  74. package/dist/tailwind-plugin-tooltip.ts +209 -0
  75. package/dist/util/EventEmitterMixin.d.ts +11 -0
  76. package/dist/util/EventEmitterMixin.d.ts.map +1 -1
  77. package/dist/util/EventEmitterMixin.js +1 -1
  78. package/dist/util/EventEmitterMixin.js.map +1 -1
  79. package/dist/util/TooltipController.d.ts +37 -0
  80. package/dist/util/TooltipController.d.ts.map +1 -0
  81. package/dist/util/TooltipController.js +133 -0
  82. package/dist/util/TooltipController.js.map +1 -0
  83. package/dist/util/TooltipMixin.d.ts +42 -0
  84. package/dist/util/TooltipMixin.d.ts.map +1 -0
  85. package/dist/util/TooltipMixin.js +401 -0
  86. package/dist/util/TooltipMixin.js.map +1 -0
  87. package/dist/util/onFind.d.ts +1 -0
  88. package/dist/util/onFind.d.ts.map +1 -1
  89. package/dist/util/onFind.js +73 -48
  90. package/dist/util/onFind.js.map +1 -1
  91. package/dist/util/onVisible.d.ts.map +1 -1
  92. package/dist/util/onVisible.js +13 -2
  93. package/dist/util/onVisible.js.map +1 -1
  94. package/package.json +12 -5
  95. package/dist/storybook/assets/AvatarGroup.stories-DrlxT-mF.js +0 -211
  96. package/dist/storybook/assets/Color-64QXVMR3-Dnd9S2a1.js +0 -1
  97. package/dist/storybook/assets/ComponentStatesMixin-C2HZ9ZFb.js +0 -1
  98. package/dist/storybook/assets/WithTooltip-SK46ZJ2J-Df0E-KJO.js +0 -825
  99. package/dist/storybook/assets/formatter-OMEEQ6HG-DFa_WTfb.js +0 -1
  100. package/dist/storybook/assets/iframe-lTczLWsL.js +0 -1064
  101. package/dist/storybook/assets/index-yMswRDPh.js +0 -1
  102. package/dist/storybook/assets/onFind-C6olvKHR.js +0 -1
  103. package/dist/storybook/assets/onFind.stories-DfW54CDE.js +0 -284
  104. package/dist/storybook/assets/syntaxhighlighter-CAVLW7PM-DoI0ixeu.js +0 -6
@@ -0,0 +1,1069 @@
1
+ import{x as h}from"./iframe-CcloOV09.js";import{l as P,o as b}from"./onFind-DqriYjEB.js";import"./preload-helper-PPVm8Dsz.js";import"./_commonjsHelpers-CqkleIqs.js";const F=Symbol.for("brightspot.onFind");let y;function v(){return y||(y=globalThis[F],y||(y={calledIndex:0,callbacks:[],blacklist:[],triggerPaused:!1,triggerOnResume:!1,triggerAll:!1,triggerElements:[],triggerFrame:0,initialized:!1},globalThis[F]=y),y)}class O{#n;#e;#t;#a;#s;constructor(t,e,n){this.#n=t,this.#e=`data-ofc${v().calledIndex++}`,Array.isArray(e)||(e=[e]),this.#t=e.map(e.some(o=>o.indexOf(",")>-1)?o=>o:o=>`${o}:not([${this.#e}])`).join(","),this.#a=n;const a=e.map(o=>o.trim()).filter(o=>o.indexOf(" ")<0&&o.indexOf(",")<0);a.length===e.length&&(this.#s=a)}addTriggerElements(t,e){if(this.#n.contains(e)){e.matches(this.#t)&&t.push(e);for(const n of e.querySelectorAll(this.#t))t.push(n)}}trigger(t){if(document.readyState!=="loading")if(this.#s&&Array.isArray(t))for(const e of t)this.#s.some(n=>e.matches(n))&&this.#o(e);else for(const e of this.#n.querySelectorAll(this.#t))this.#o(e)}#o(t){if(!H(t)&&!t.hasAttribute(this.#e)){t.setAttribute(this.#e,"");try{this.#a(t)}catch(e){P.error("Failed callback!",t,e)}}}}function H(s){if(!(s instanceof Element))return!1;const{blacklist:t}=v();return t.length>0&&t.some(e=>s.closest(e)!==null)}const $=Array.prototype.every,S=s=>s.nodeType===Node.TEXT_NODE;function C(s){const t=v();if(t.triggerPaused){t.triggerOnResume=!0;return}const e=Array.isArray(s),n=[];if(e){for(const a of s){const o=a.target;if(!H(o))switch(a.type){case"attributes":a.oldValue!==o.getAttribute(a.attributeName)&&n.push(a);break;case"childList":(!$.call(a.addedNodes,S)||!$.call(a.removedNodes,S))&&n.push(a);break}}if(n.length===0)return}if(n.length>500&&(t.triggerAll=!0),!t.triggerAll)if(e)for(const a of n){const o=a.target;switch(a.type){case"attributes":t.triggerElements.push(o);break;case"childList":for(const r of t.callbacks)r.addTriggerElements(t.triggerElements,o);break}}else t.triggerAll=!0;(t.triggerAll||t.triggerElements.length>0)&&!t.triggerFrame&&(t.triggerFrame=window.requestAnimationFrame(()=>{const a=t.triggerAll?void 0:t.triggerElements;t.triggerAll=!1,t.triggerElements=[],t.triggerFrame=0,t.callbacks.forEach(o=>o.trigger(a))}))}function z(){C(),new MutationObserver(C).observe(document,{attributes:!0,attributeFilter:["class","data-bsp-autosubmit","data-chart-type","data-code-type","data-internal-name","data-tab","name","rel","target"],attributeOldValue:!0,childList:!0,subtree:!0})}function D(){const s=v();s.initialized||(s.initialized=!0,document.readyState==="loading"?document.addEventListener("DOMContentLoaded",z):z())}function m(s,t,e){D();let n,a;typeof e<"u"?(n=s,a=t):(n=document,a=s,e=t);const o=new O(n,a,e);o.trigger(),v().callbacks.push(o)}m.triggerCallbacks=C;m.pause=()=>{v().triggerPaused=!0};m.resume=()=>{const s=v();s.triggerPaused=!1,s.triggerOnResume&&(s.triggerOnResume=!1,setTimeout(C,100))};m.ignore=(...s)=>{const{blacklist:t}=v();for(const e of s){try{document.querySelector(e)}catch{continue}t.includes(e)||t.push(e)}};const{expect:d,userEvent:c,waitFor:u,within:k}=__STORYBOOK_MODULE_TEST__,U={title:"Utilities/onFind",tags:["autodocs"],parameters:{docs:{subtitle:"The `onFind` utility observes DOM mutations and executes callbacks when elements matching specified selectors appear. Uses MutationObserver internally to efficiently track element additions and attribute changes."},controls:{expanded:!0}},argTypes:{selector:{control:{type:"text"},description:"CSS selector to watch for"}},args:{selector:".dynamic-item"}},f={render:s=>{const t=`onfind-${Math.random().toString(36).substring(2,9)}`,e=`.dynamic-item-${t}`;let n=0,a=0;const o=()=>{const i=document.getElementById(`${t}-items-count`),l=document.getElementById(`${t}-found-count`);i&&(i.textContent=String(n)),l&&(l.textContent=String(a))};return b(e,i=>{a++,o(),i.classList.add("ring-2","ring-success-500")}),h`
2
+ <div class="space-y-4">
3
+ <div class="text-base">
4
+ <p class="mb-2">
5
+ Click "Add Item" to dynamically create elements. The onFind observer detects them and highlights them with a
6
+ green border.
7
+ </p>
8
+ </div>
9
+
10
+ <div class="flex gap-2">
11
+ <button
12
+ data-testid="add-item"
13
+ @click=${()=>{n++;const i=document.getElementById(`${t}-container`);if(i){const l=document.createElement("div");l.className=`dynamic-item-${t} rounded border bg-white p-4`,l.textContent=`Item ${n}`,i.appendChild(l)}o()}}
14
+ class="bg-primary-500 hover:bg-primary-600 rounded px-4 py-2 text-white"
15
+ >
16
+ Add Item
17
+ </button>
18
+ <button
19
+ data-testid="clear-items"
20
+ @click=${()=>{n=0,a=0;const i=document.getElementById(`${t}-container`);i&&(i.innerHTML=""),o()}}
21
+ class="rounded bg-gray-200 px-4 py-2 hover:bg-gray-300"
22
+ >
23
+ Clear All
24
+ </button>
25
+ </div>
26
+
27
+ <div id="${t}-container" class="min-h-32 space-y-2 rounded border-2 border-gray-300 bg-gray-50 p-4">
28
+ <div class="text-sm text-gray-500">Items will appear here...</div>
29
+ </div>
30
+
31
+ <div class="flex justify-around">
32
+ <div>
33
+ <div class="text-xs text-gray-500">Items created</div>
34
+ <div data-testid="items-count" id="${t}-items-count" class="text-2xl font-bold text-gray-900">
35
+ 0
36
+ </div>
37
+ </div>
38
+ <div>
39
+ <div class="text-xs text-gray-500">Items detected by onFind</div>
40
+ <div data-testid="found-count" id="${t}-found-count" class="text-primary-600 text-2xl font-bold">
41
+ 0
42
+ </div>
43
+ </div>
44
+ </div>
45
+ </div>
46
+ `},parameters:{docs:{description:{story:"Interactive example showing onFind detecting dynamically added elements. Elements are automatically highlighted when detected."}}},play:async({canvasElement:s,step:t})=>{const e=k(s);await t("Detects dynamically added element",async()=>{await c.click(e.getByTestId("add-item")),await u(()=>d(e.getByTestId("found-count")).toHaveTextContent("1"))}),await t("Detects multiple elements",async()=>{await c.click(e.getByTestId("add-item")),await c.click(e.getByTestId("add-item")),await u(()=>d(e.getByTestId("found-count")).toHaveTextContent("3"))}),await t("Clear resets counts",async()=>{await c.click(e.getByTestId("clear-items")),d(e.getByTestId("items-count")).toHaveTextContent("0"),d(e.getByTestId("found-count")).toHaveTextContent("0")}),await t("Detection resumes after clear",async()=>{await c.click(e.getByTestId("add-item")),await u(()=>d(e.getByTestId("found-count")).toHaveTextContent("1"))})}},B={render:()=>{const s="onfind-ignore-demo",t=".ignore-demo-item",e="ignore-demo-zone";let n=0,a=0;const o=()=>{const i=document.getElementById(`${s}-found-count`);i&&(i.textContent=String(n))};return b.ignore(`.${e}`),b(t,i=>{n++,o(),i.classList.add("ring-2","ring-success-500")}),h`
47
+ <div class="space-y-4">
48
+ <p class="text-base">
49
+ Elements added inside the <strong>ignored zone</strong> are skipped by onFind callbacks. Elements added
50
+ outside are detected normally.
51
+ </p>
52
+
53
+ <div class="flex gap-2">
54
+ <button
55
+ data-testid="add-outside"
56
+ @click=${()=>{a++;const i=document.getElementById(`${s}-outside`);if(i){const l=document.createElement("div");l.className=`${t.slice(1)} rounded border bg-white p-4`,l.textContent=`Outside ${a}`,i.appendChild(l)}}}
57
+ class="bg-primary-500 hover:bg-primary-600 rounded px-4 py-2 text-white"
58
+ >
59
+ Add Outside
60
+ </button>
61
+ <button data-testid="add-inside" @click=${()=>{a++;const i=document.getElementById(`${s}-inside`);if(i){const l=document.createElement("div");l.className=`${t.slice(1)} rounded border bg-white p-4`,l.textContent=`Inside ${a}`,i.appendChild(l)}}} class="rounded bg-gray-200 px-4 py-2 hover:bg-gray-300">
62
+ Add Inside Ignored
63
+ </button>
64
+ </div>
65
+
66
+ <div class="grid grid-cols-2 gap-4">
67
+ <div>
68
+ <div class="mb-1 text-xs font-semibold text-gray-500">Normal zone</div>
69
+ <div id="${s}-outside" class="min-h-32 space-y-2 rounded border-2 border-gray-300 bg-gray-50 p-4">
70
+ <div class="text-sm text-gray-400">Items detected here</div>
71
+ </div>
72
+ </div>
73
+ <div>
74
+ <div class="mb-1 text-xs font-semibold text-gray-500">Ignored zone</div>
75
+ <div
76
+ id="${s}-inside"
77
+ class="${e} min-h-32 space-y-2 rounded border-2 border-red-300 bg-red-50 p-4"
78
+ >
79
+ <div class="text-sm text-gray-400">Items ignored here</div>
80
+ </div>
81
+ </div>
82
+ </div>
83
+
84
+ <div>
85
+ <div class="text-xs text-gray-500">Items detected by onFind</div>
86
+ <div
87
+ data-testid="ignore-found-count"
88
+ id="${s}-found-count"
89
+ class="text-primary-600 text-2xl font-bold"
90
+ >
91
+ 0
92
+ </div>
93
+ </div>
94
+ </div>
95
+ `},parameters:{docs:{description:{story:"Demonstrates `onFind.ignore()` — elements inside ignored containers are excluded from callback processing. Useful for skipping rich-text editors or other zones where DOM mutations should not trigger component initialization."}}},play:async({canvasElement:s,step:t})=>{const e=k(s);await t("Detects outside ignored zone",async()=>{await c.click(e.getByTestId("add-outside")),await u(()=>d(e.getByTestId("ignore-found-count")).toHaveTextContent("1"))}),await t("Skips inside ignored zone",async()=>{await c.click(e.getByTestId("add-inside")),await new Promise(n=>requestAnimationFrame(()=>requestAnimationFrame(n))),d(e.getByTestId("ignore-found-count")).toHaveTextContent("1")}),await t("Continues detecting outside",async()=>{await c.click(e.getByTestId("add-outside")),await u(()=>d(e.getByTestId("ignore-found-count")).toHaveTextContent("2"))})}},w={render:()=>{const s="onfind-pause-demo",t=".pause-demo-item";let e=0,n=0;const a=()=>{const i=document.getElementById(`${s}-found-count`);i&&(i.textContent=String(e))};b(t,i=>{e++,a(),i.classList.add("ring-2","ring-success-500")});const o=()=>{n++;const i=document.getElementById(`${s}-container`);if(i){const l=document.createElement("div");l.className=`${t.slice(1)} rounded border bg-white p-4`,l.textContent=`Item ${n}`,i.appendChild(l)}};let r=!1;return h`
96
+ <div class="space-y-4">
97
+ <p class="text-base">
98
+ Use <code>onFind.pause()</code> to temporarily stop detection and <code>onFind.resume()</code> to restart it.
99
+ Elements added while paused are detected on resume.
100
+ </p>
101
+
102
+ <div class="flex gap-2">
103
+ <button data-testid="pause-add" @click=${o} class="btu-button btu-button-primary btu-button-sm">
104
+ Add Item
105
+ </button>
106
+ <button data-testid="toggle-pause" @click=${()=>{r=!r,r?b.pause():b.resume();const i=document.querySelector('[data-testid="toggle-pause"]');i&&(i.textContent=r?"Resume":"Pause",i.className=r?"btu-button btu-button-success btu-button-sm":"btu-button btu-button-warning btu-button-sm")}} class="btu-button btu-button-warning btu-button-sm">
107
+ Pause
108
+ </button>
109
+ </div>
110
+
111
+ <div id="${s}-container" class="min-h-32 space-y-2 rounded border-2 border-gray-300 bg-gray-50 p-4">
112
+ <div class="text-sm text-gray-500">Items will appear here...</div>
113
+ </div>
114
+
115
+ <div>
116
+ <div class="text-xs text-gray-500">Items detected by onFind</div>
117
+ <div
118
+ data-testid="pause-found-count"
119
+ id="${s}-found-count"
120
+ class="text-primary-600 text-2xl font-bold"
121
+ >
122
+ 0
123
+ </div>
124
+ </div>
125
+ </div>
126
+ `},parameters:{docs:{description:{story:"Demonstrates `onFind.pause()` and `onFind.resume()`. While paused, DOM mutations are queued and processed when resumed."}}},play:async({canvasElement:s,step:t})=>{const e=k(s);await t("Normal detection before pause",async()=>{await c.click(e.getByTestId("pause-add")),await u(()=>d(e.getByTestId("pause-found-count")).toHaveTextContent("1"))}),await t("Pause stops detection",async()=>{await c.click(e.getByTestId("toggle-pause")),await c.click(e.getByTestId("pause-add")),await new Promise(n=>setTimeout(n,200)),d(e.getByTestId("pause-found-count")).toHaveTextContent("1")}),await t("Resume triggers pending detection",async()=>{await c.click(e.getByTestId("toggle-pause")),await u(()=>d(e.getByTestId("pause-found-count")).toHaveTextContent("2"),{timeout:2e3})})}},I={render:()=>{const s=b,t=".singleton-item-a",e=".singleton-item-b";let n=0,a=0;const o=()=>{const g=document.getElementById("singleton-count-a"),x=document.getElementById("singleton-count-b");g&&(g.textContent=String(n)),x&&(x.textContent=String(a))};s.ignore(".singleton-ignored-zone"),s(t,g=>{n++,o(),g.classList.add("ring-2","ring-blue-500")}),m(e,g=>{a++,o(),g.classList.add("ring-2","ring-purple-500")});const r=(g,x,M)=>{const A=document.getElementById(M);if(A){const E=document.createElement("div");E.className=`${g.slice(1)} rounded border bg-white p-4`,E.textContent=x,A.appendChild(E)}};let p=!1;return h`
127
+ <div class="space-y-4">
128
+ <p class="text-base">
129
+ Two separate ES module instances of <code>onFind</code> (each with its own <code>_state</code> cache) share
130
+ one observer, ignore list, and pause/resume state via <code>globalThis[Symbol.for()]</code>.
131
+ </p>
132
+
133
+ <div class="flex items-center gap-4">
134
+ <button
135
+ data-testid="singleton-toggle-pause"
136
+ @click=${()=>{p=!p,p?s.pause():s.resume();const g=document.querySelector('[data-testid="singleton-toggle-pause"]');g&&(g.textContent=p?"Resume":"Pause",g.className=p?"btu-button btu-button-success btu-button-sm":"btu-button btu-button-warning btu-button-sm")}}
137
+ class="btu-button btu-button-warning btu-button-sm"
138
+ >
139
+ Pause
140
+ </button>
141
+ <div class="flex items-center gap-1 text-sm text-blue-600">
142
+ Bundle A:
143
+ <span data-testid="singleton-count-a" id="singleton-count-a" class="font-bold">0</span>
144
+ items found
145
+ </div>
146
+ <div class="flex items-center gap-1 text-sm text-purple-600">
147
+ Bundle B:
148
+ <span data-testid="singleton-count-b" id="singleton-count-b" class="font-bold">0</span>
149
+ items found
150
+ </div>
151
+ </div>
152
+
153
+ <div class="grid grid-cols-3 gap-4">
154
+ <div>
155
+ <div class="mb-2 text-xs font-semibold text-blue-600">Bundle A zone</div>
156
+ <div class="mb-2 flex gap-1">
157
+ <button
158
+ data-testid="singleton-add-a"
159
+ @click=${()=>r(t,"Bundle A item","singleton-zone-a")}
160
+ class="btu-button btu-button-primary btu-button-sm"
161
+ >
162
+ Add A
163
+ </button>
164
+ <button
165
+ data-testid="singleton-zone-a-add-b"
166
+ @click=${()=>r(e,"Bundle B item","singleton-zone-a")}
167
+ class="btu-button btu-button-purple btu-button-sm"
168
+ >
169
+ Add B
170
+ </button>
171
+ </div>
172
+ <div id="singleton-zone-a" class="min-h-24 space-y-2 rounded border-2 border-blue-300 bg-blue-50 p-4">
173
+ <div class="text-sm text-gray-400">A items here</div>
174
+ </div>
175
+ </div>
176
+ <div>
177
+ <div class="mb-2 text-xs font-semibold text-purple-600">Bundle B zone</div>
178
+ <div class="mb-2 flex gap-1">
179
+ <button
180
+ data-testid="singleton-zone-b-add-a"
181
+ @click=${()=>r(t,"Bundle A item","singleton-zone-b")}
182
+ class="btu-button btu-button-primary btu-button-sm"
183
+ >
184
+ Add A
185
+ </button>
186
+ <button
187
+ data-testid="singleton-add-b"
188
+ @click=${()=>r(e,"Bundle B item","singleton-zone-b")}
189
+ class="btu-button btu-button-purple btu-button-sm"
190
+ >
191
+ Add B
192
+ </button>
193
+ </div>
194
+ <div id="singleton-zone-b" class="min-h-24 space-y-2 rounded border-2 border-purple-300 bg-purple-50 p-4">
195
+ <div class="text-sm text-gray-400">B items here</div>
196
+ </div>
197
+ </div>
198
+ <div>
199
+ <div class="mb-2 text-xs font-semibold text-red-600">Ignored zone</div>
200
+ <div class="mb-2 flex gap-1">
201
+ <button
202
+ data-testid="singleton-add-ignored"
203
+ @click=${()=>{r(t,"Ignored A","singleton-ignored"),r(e,"Ignored B","singleton-ignored")}}
204
+ class="btu-button btu-button-gray btu-button-sm"
205
+ >
206
+ Add A & B
207
+ </button>
208
+ </div>
209
+ <div
210
+ id="singleton-ignored"
211
+ class="singleton-ignored-zone min-h-24 space-y-2 rounded border-2 border-red-300 bg-red-50 p-4"
212
+ >
213
+ <div class="text-sm text-gray-400">Items ignored here</div>
214
+ </div>
215
+ </div>
216
+ </div>
217
+ </div>
218
+ `},parameters:{docs:{description:{story:"Two independent module instances of `onFind` (simulating separate bundles) share a single MutationObserver, ignore list, and pause/resume state through the `globalThis[Symbol.for()]` singleton pattern."}}},play:async({canvasElement:s,step:t})=>{const e=k(s);await t("Shared observer: Bundle A detects A items, Bundle B detects B items",async()=>{await c.click(e.getByTestId("singleton-add-a")),await u(()=>d(e.getByTestId("singleton-count-a")).toHaveTextContent("1")),await c.click(e.getByTestId("singleton-add-b")),await u(()=>d(e.getByTestId("singleton-count-b")).toHaveTextContent("1"))}),await t("Cross-zone detection fires correct callback",async()=>{await c.click(e.getByTestId("singleton-zone-a-add-b")),await u(()=>d(e.getByTestId("singleton-count-b")).toHaveTextContent("2")),d(e.getByTestId("singleton-count-a")).toHaveTextContent("1"),await c.click(e.getByTestId("singleton-zone-b-add-a")),await u(()=>d(e.getByTestId("singleton-count-a")).toHaveTextContent("2")),d(e.getByTestId("singleton-count-b")).toHaveTextContent("2")}),await t("Shared ignore list: Bundle A ignore() blocks Bundle B callbacks",async()=>{const n=e.getByTestId("singleton-count-a").textContent,a=e.getByTestId("singleton-count-b").textContent;await c.click(e.getByTestId("singleton-add-ignored")),await new Promise(o=>requestAnimationFrame(()=>requestAnimationFrame(o))),d(e.getByTestId("singleton-count-a")).toHaveTextContent(n),d(e.getByTestId("singleton-count-b")).toHaveTextContent(a)}),await t("Ignore lists are merged and deduped across bundles",async()=>{const n=globalThis[Symbol.for("brightspot.onFind")],a=n.blacklist.length;m.ignore(".singleton-ignored-zone"),d(n.blacklist.length).toBe(a),m.ignore(".bundle-b-ignored"),d(n.blacklist).toContain(".singleton-ignored-zone"),d(n.blacklist).toContain(".bundle-b-ignored"),d(n.blacklist.length).toBe(a+1)}),await t("Shared pause/resume: Bundle A pause() stops Bundle B detection",async()=>{await c.click(e.getByTestId("singleton-toggle-pause")),await c.click(e.getByTestId("singleton-add-a")),await c.click(e.getByTestId("singleton-add-b")),await new Promise(n=>setTimeout(n,200)),d(e.getByTestId("singleton-count-a")).toHaveTextContent("2"),d(e.getByTestId("singleton-count-b")).toHaveTextContent("2"),await c.click(e.getByTestId("singleton-toggle-pause")),await u(()=>d(e.getByTestId("singleton-count-a")).toHaveTextContent("3"),{timeout:2e3}),await u(()=>d(e.getByTestId("singleton-count-b")).toHaveTextContent("3"),{timeout:2e3})}),await t("No index collision on data-ofc attributes",async()=>{const n=s.querySelectorAll(".singleton-item-a, .singleton-item-b"),a=new Set;n.forEach(o=>{o.getAttributeNames().filter(r=>r.startsWith("data-ofc")).forEach(r=>a.add(r))}),d(a.size).toBeGreaterThanOrEqual(2)}),await t("Singleton state: both bundles share the same globalThis object",async()=>{const n=globalThis[Symbol.for("brightspot.onFind")];d(n).toBeDefined(),d(n.callbacks).toBeDefined(),d(Array.isArray(n.callbacks)).toBe(!0),d(n.initialized).toBe(!0),d(b.pause).not.toBe(m.pause),d(b.resume).not.toBe(m.resume)})}},T={render:()=>h`
219
+ <div class="space-y-4 text-sm">
220
+ <div>
221
+ <h3 class="mb-2 font-bold">Basic Usage</h3>
222
+ <pre
223
+ class="overflow-x-auto rounded bg-gray-900 p-4 text-gray-100"
224
+ ><code>import onFind from '@brightspot/ui/util/onFind.js'
225
+
226
+ // Watch for elements globally
227
+ onFind('.my-component', (element) => {
228
+ console.log('Found element:', element)
229
+ // Initialize component
230
+ })
231
+
232
+ // Watch within a specific root element
233
+ const container = document.querySelector('#container')
234
+ onFind(container, '.my-widget', (element) => {
235
+ // Initialize widget
236
+ })</code></pre>
237
+ </div>
238
+
239
+ <div>
240
+ <h3 class="mb-2 font-bold">Multiple Selectors</h3>
241
+ <pre class="overflow-x-auto rounded bg-gray-900 p-4 text-gray-100"><code>// Watch for multiple selectors at once
242
+ onFind(['.button', '.input', '.select'], (element) => {
243
+ if (element.matches('.button')) {
244
+ // Handle button
245
+ } else if (element.matches('.input')) {
246
+ // Handle input
247
+ }
248
+ })</code></pre>
249
+ </div>
250
+
251
+ <div>
252
+ <h3 class="mb-2 font-bold">Pause and Resume</h3>
253
+ <pre class="overflow-x-auto rounded bg-gray-900 p-4 text-gray-100"><code>// Pause observations temporarily
254
+ onFind.pause()
255
+
256
+ // Make bulk DOM changes without triggering callbacks
257
+ // ...
258
+
259
+ // Resume observations
260
+ onFind.resume()</code></pre>
261
+ </div>
262
+
263
+ <div>
264
+ <h3 class="mb-2 font-bold">Ignore Selectors</h3>
265
+ <pre
266
+ class="overflow-x-auto rounded bg-gray-900 p-4 text-gray-100"
267
+ ><code>// Exclude elements inside matching containers from all callbacks.
268
+ // Calls are additive and shared across bundles.
269
+ onFind.ignore('.ProseMirror', '.RichTextEditor')
270
+
271
+ // Elements inside ignored containers will not trigger callbacks,
272
+ // even if they match a registered selector.</code></pre>
273
+ </div>
274
+
275
+ <div>
276
+ <h3 class="mb-2 font-bold">Common Use Cases</h3>
277
+ <ul class="list-inside list-disc space-y-1 text-gray-700">
278
+ <li>Initialize components when they appear in the DOM</li>
279
+ <li>Attach event listeners to dynamically created elements</li>
280
+ <li>Enhance server-rendered content progressively</li>
281
+ <li>Detect and respond to attribute changes</li>
282
+ <li>Apply transformations to newly added content</li>
283
+ </ul>
284
+ </div>
285
+
286
+ <div>
287
+ <h3 class="mb-2 font-bold">Parameters</h3>
288
+ <ul class="space-y-2">
289
+ <li>
290
+ <code class="rounded bg-gray-200 px-1">root</code> (ParentNode, optional): Root element to observe. Defaults
291
+ to document.
292
+ </li>
293
+ <li>
294
+ <code class="rounded bg-gray-200 px-1">selectors</code> (string | string[]): CSS selector(s) to watch for
295
+ </li>
296
+ <li>
297
+ <code class="rounded bg-gray-200 px-1">fn</code> (function): Callback executed when matching elements are
298
+ found
299
+ </li>
300
+ </ul>
301
+ </div>
302
+
303
+ <div>
304
+ <h3 class="mb-2 font-bold">Static Methods</h3>
305
+ <table class="w-full text-left text-sm">
306
+ <thead>
307
+ <tr class="border-b">
308
+ <th class="py-2 pr-4">Method</th>
309
+ <th class="py-2 pr-4">Signature</th>
310
+ <th class="py-2">Description</th>
311
+ </tr>
312
+ </thead>
313
+ <tbody>
314
+ <tr class="border-b">
315
+ <td class="py-2 pr-4"><code class="rounded bg-gray-200 px-1">onFind.ignore</code></td>
316
+ <td class="py-2 pr-4"><code class="text-xs">(...selectors: string[]) =&gt; void</code></td>
317
+ <td class="py-2">
318
+ Exclude elements inside matching containers from all callbacks. Calls are additive and shared across
319
+ bundles.
320
+ </td>
321
+ </tr>
322
+ <tr class="border-b">
323
+ <td class="py-2 pr-4"><code class="rounded bg-gray-200 px-1">onFind.pause</code></td>
324
+ <td class="py-2 pr-4"><code class="text-xs">() =&gt; void</code></td>
325
+ <td class="py-2">Pause mutation processing. Mutations that occur while paused trigger on resume.</td>
326
+ </tr>
327
+ <tr class="border-b">
328
+ <td class="py-2 pr-4"><code class="rounded bg-gray-200 px-1">onFind.resume</code></td>
329
+ <td class="py-2 pr-4"><code class="text-xs">() =&gt; void</code></td>
330
+ <td class="py-2">Resume mutation processing after a pause.</td>
331
+ </tr>
332
+ <tr>
333
+ <td class="py-2 pr-4"><code class="rounded bg-gray-200 px-1">onFind.triggerCallbacks</code></td>
334
+ <td class="py-2 pr-4"><code class="text-xs">(mutations?: MutationRecord[]) =&gt; void</code></td>
335
+ <td class="py-2">Manually trigger callback evaluation for all registered selectors.</td>
336
+ </tr>
337
+ </tbody>
338
+ </table>
339
+ </div>
340
+
341
+ <div>
342
+ <h3 class="mb-2 font-bold">How It Works</h3>
343
+ <p class="text-gray-700">
344
+ onFind uses MutationObserver to efficiently watch for DOM changes. Each callback is executed only once per
345
+ element using data attributes to track already-processed elements. The observer watches for childList
346
+ mutations and specific attribute changes. Use <code class="rounded bg-gray-200 px-1">onFind.ignore()</code>
347
+ to exclude containers (e.g., rich-text editors) from triggering callbacks.
348
+ </p>
349
+ </div>
350
+ </div>
351
+ `,parameters:{docs:{description:{story:"Code examples and documentation for using the onFind utility."}}}};f.parameters={...f.parameters,docs:{...f.parameters?.docs,source:{originalSource:`{
352
+ render: args => {
353
+ const instanceId = \`onfind-\${Math.random().toString(36).substring(2, 9)}\`;
354
+ const uniqueSelector = \`.dynamic-item-\${instanceId}\`;
355
+ let itemCount = 0;
356
+ let foundCount = 0;
357
+ const updateCounts = () => {
358
+ const itemsEl = document.getElementById(\`\${instanceId}-items-count\`);
359
+ const foundEl = document.getElementById(\`\${instanceId}-found-count\`);
360
+ if (itemsEl) itemsEl.textContent = String(itemCount);
361
+ if (foundEl) foundEl.textContent = String(foundCount);
362
+ };
363
+
364
+ // Set up onFind observer for dynamic items with unique selector
365
+ onFind(uniqueSelector, (element: HTMLElement) => {
366
+ foundCount++;
367
+ updateCounts();
368
+ element.classList.add('ring-2', 'ring-success-500');
369
+ });
370
+ const addItem = () => {
371
+ itemCount++;
372
+ const container = document.getElementById(\`\${instanceId}-container\`);
373
+ if (container) {
374
+ const item = document.createElement('div');
375
+ item.className = \`dynamic-item-\${instanceId} rounded border bg-white p-4\`;
376
+ item.textContent = \`Item \${itemCount}\`;
377
+ container.appendChild(item);
378
+ }
379
+ updateCounts();
380
+ };
381
+ const clearItems = () => {
382
+ itemCount = 0;
383
+ foundCount = 0;
384
+ const container = document.getElementById(\`\${instanceId}-container\`);
385
+ if (container) {
386
+ container.innerHTML = '';
387
+ }
388
+ updateCounts();
389
+ };
390
+ return html\`
391
+ <div class="space-y-4">
392
+ <div class="text-base">
393
+ <p class="mb-2">
394
+ Click "Add Item" to dynamically create elements. The onFind observer detects them and highlights them with a
395
+ green border.
396
+ </p>
397
+ </div>
398
+
399
+ <div class="flex gap-2">
400
+ <button
401
+ data-testid="add-item"
402
+ @click=\${addItem}
403
+ class="bg-primary-500 hover:bg-primary-600 rounded px-4 py-2 text-white"
404
+ >
405
+ Add Item
406
+ </button>
407
+ <button
408
+ data-testid="clear-items"
409
+ @click=\${clearItems}
410
+ class="rounded bg-gray-200 px-4 py-2 hover:bg-gray-300"
411
+ >
412
+ Clear All
413
+ </button>
414
+ </div>
415
+
416
+ <div id="\${instanceId}-container" class="min-h-32 space-y-2 rounded border-2 border-gray-300 bg-gray-50 p-4">
417
+ <div class="text-sm text-gray-500">Items will appear here...</div>
418
+ </div>
419
+
420
+ <div class="flex justify-around">
421
+ <div>
422
+ <div class="text-xs text-gray-500">Items created</div>
423
+ <div data-testid="items-count" id="\${instanceId}-items-count" class="text-2xl font-bold text-gray-900">
424
+ 0
425
+ </div>
426
+ </div>
427
+ <div>
428
+ <div class="text-xs text-gray-500">Items detected by onFind</div>
429
+ <div data-testid="found-count" id="\${instanceId}-found-count" class="text-primary-600 text-2xl font-bold">
430
+ 0
431
+ </div>
432
+ </div>
433
+ </div>
434
+ </div>
435
+ \`;
436
+ },
437
+ parameters: {
438
+ docs: {
439
+ description: {
440
+ story: \`Interactive example showing onFind detecting dynamically added elements. Elements are automatically highlighted when detected.\`
441
+ }
442
+ }
443
+ },
444
+ play: async ({
445
+ canvasElement,
446
+ step
447
+ }) => {
448
+ const canvas = within(canvasElement);
449
+ await step('Detects dynamically added element', async () => {
450
+ await userEvent.click(canvas.getByTestId('add-item'));
451
+ await waitFor(() => expect(canvas.getByTestId('found-count')).toHaveTextContent('1'));
452
+ });
453
+ await step('Detects multiple elements', async () => {
454
+ await userEvent.click(canvas.getByTestId('add-item'));
455
+ await userEvent.click(canvas.getByTestId('add-item'));
456
+ await waitFor(() => expect(canvas.getByTestId('found-count')).toHaveTextContent('3'));
457
+ });
458
+ await step('Clear resets counts', async () => {
459
+ await userEvent.click(canvas.getByTestId('clear-items'));
460
+ expect(canvas.getByTestId('items-count')).toHaveTextContent('0');
461
+ expect(canvas.getByTestId('found-count')).toHaveTextContent('0');
462
+ });
463
+ await step('Detection resumes after clear', async () => {
464
+ await userEvent.click(canvas.getByTestId('add-item'));
465
+ await waitFor(() => expect(canvas.getByTestId('found-count')).toHaveTextContent('1'));
466
+ });
467
+ }
468
+ }`,...f.parameters?.docs?.source}}};B.parameters={...B.parameters,docs:{...B.parameters?.docs,source:{originalSource:`{
469
+ render: () => {
470
+ const instanceId = 'onfind-ignore-demo';
471
+ const itemSelector = '.ignore-demo-item';
472
+ const ignoredClass = 'ignore-demo-zone';
473
+ let foundCount = 0;
474
+ let itemCount = 0;
475
+ const updateCount = () => {
476
+ const el = document.getElementById(\`\${instanceId}-found-count\`);
477
+ if (el) el.textContent = String(foundCount);
478
+ };
479
+ onFind.ignore(\`.\${ignoredClass}\`);
480
+ onFind(itemSelector, (element: HTMLElement) => {
481
+ foundCount++;
482
+ updateCount();
483
+ element.classList.add('ring-2', 'ring-success-500');
484
+ });
485
+ const addOutside = () => {
486
+ itemCount++;
487
+ const container = document.getElementById(\`\${instanceId}-outside\`);
488
+ if (container) {
489
+ const item = document.createElement('div');
490
+ item.className = \`\${itemSelector.slice(1)} rounded border bg-white p-4\`;
491
+ item.textContent = \`Outside \${itemCount}\`;
492
+ container.appendChild(item);
493
+ }
494
+ };
495
+ const addInside = () => {
496
+ itemCount++;
497
+ const container = document.getElementById(\`\${instanceId}-inside\`);
498
+ if (container) {
499
+ const item = document.createElement('div');
500
+ item.className = \`\${itemSelector.slice(1)} rounded border bg-white p-4\`;
501
+ item.textContent = \`Inside \${itemCount}\`;
502
+ container.appendChild(item);
503
+ }
504
+ };
505
+ return html\`
506
+ <div class="space-y-4">
507
+ <p class="text-base">
508
+ Elements added inside the <strong>ignored zone</strong> are skipped by onFind callbacks. Elements added
509
+ outside are detected normally.
510
+ </p>
511
+
512
+ <div class="flex gap-2">
513
+ <button
514
+ data-testid="add-outside"
515
+ @click=\${addOutside}
516
+ class="bg-primary-500 hover:bg-primary-600 rounded px-4 py-2 text-white"
517
+ >
518
+ Add Outside
519
+ </button>
520
+ <button data-testid="add-inside" @click=\${addInside} class="rounded bg-gray-200 px-4 py-2 hover:bg-gray-300">
521
+ Add Inside Ignored
522
+ </button>
523
+ </div>
524
+
525
+ <div class="grid grid-cols-2 gap-4">
526
+ <div>
527
+ <div class="mb-1 text-xs font-semibold text-gray-500">Normal zone</div>
528
+ <div id="\${instanceId}-outside" class="min-h-32 space-y-2 rounded border-2 border-gray-300 bg-gray-50 p-4">
529
+ <div class="text-sm text-gray-400">Items detected here</div>
530
+ </div>
531
+ </div>
532
+ <div>
533
+ <div class="mb-1 text-xs font-semibold text-gray-500">Ignored zone</div>
534
+ <div
535
+ id="\${instanceId}-inside"
536
+ class="\${ignoredClass} min-h-32 space-y-2 rounded border-2 border-red-300 bg-red-50 p-4"
537
+ >
538
+ <div class="text-sm text-gray-400">Items ignored here</div>
539
+ </div>
540
+ </div>
541
+ </div>
542
+
543
+ <div>
544
+ <div class="text-xs text-gray-500">Items detected by onFind</div>
545
+ <div
546
+ data-testid="ignore-found-count"
547
+ id="\${instanceId}-found-count"
548
+ class="text-primary-600 text-2xl font-bold"
549
+ >
550
+ 0
551
+ </div>
552
+ </div>
553
+ </div>
554
+ \`;
555
+ },
556
+ parameters: {
557
+ docs: {
558
+ description: {
559
+ story: \`Demonstrates \\\`onFind.ignore()\\\` — elements inside ignored containers are excluded from callback processing. Useful for skipping rich-text editors or other zones where DOM mutations should not trigger component initialization.\`
560
+ }
561
+ }
562
+ },
563
+ play: async ({
564
+ canvasElement,
565
+ step
566
+ }) => {
567
+ const canvas = within(canvasElement);
568
+ await step('Detects outside ignored zone', async () => {
569
+ await userEvent.click(canvas.getByTestId('add-outside'));
570
+ await waitFor(() => expect(canvas.getByTestId('ignore-found-count')).toHaveTextContent('1'));
571
+ });
572
+ await step('Skips inside ignored zone', async () => {
573
+ await userEvent.click(canvas.getByTestId('add-inside'));
574
+ // Wait a frame to ensure mutation observer has processed
575
+ await new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve)));
576
+ expect(canvas.getByTestId('ignore-found-count')).toHaveTextContent('1');
577
+ });
578
+ await step('Continues detecting outside', async () => {
579
+ await userEvent.click(canvas.getByTestId('add-outside'));
580
+ await waitFor(() => expect(canvas.getByTestId('ignore-found-count')).toHaveTextContent('2'));
581
+ });
582
+ }
583
+ }`,...B.parameters?.docs?.source}}};w.parameters={...w.parameters,docs:{...w.parameters?.docs,source:{originalSource:`{
584
+ render: () => {
585
+ const instanceId = 'onfind-pause-demo';
586
+ const itemSelector = '.pause-demo-item';
587
+ let foundCount = 0;
588
+ let itemCount = 0;
589
+ const updateCount = () => {
590
+ const el = document.getElementById(\`\${instanceId}-found-count\`);
591
+ if (el) el.textContent = String(foundCount);
592
+ };
593
+ onFind(itemSelector, (element: HTMLElement) => {
594
+ foundCount++;
595
+ updateCount();
596
+ element.classList.add('ring-2', 'ring-success-500');
597
+ });
598
+ const addItem = () => {
599
+ itemCount++;
600
+ const container = document.getElementById(\`\${instanceId}-container\`);
601
+ if (container) {
602
+ const item = document.createElement('div');
603
+ item.className = \`\${itemSelector.slice(1)} rounded border bg-white p-4\`;
604
+ item.textContent = \`Item \${itemCount}\`;
605
+ container.appendChild(item);
606
+ }
607
+ };
608
+ let paused = false;
609
+ const togglePause = () => {
610
+ paused = !paused;
611
+ if (paused) {
612
+ onFind.pause();
613
+ } else {
614
+ onFind.resume();
615
+ }
616
+ const btn = document.querySelector('[data-testid="toggle-pause"]') as HTMLButtonElement;
617
+ if (btn) {
618
+ btn.textContent = paused ? 'Resume' : 'Pause';
619
+ btn.className = paused ? 'btu-button btu-button-success btu-button-sm' : 'btu-button btu-button-warning btu-button-sm';
620
+ }
621
+ };
622
+ return html\`
623
+ <div class="space-y-4">
624
+ <p class="text-base">
625
+ Use <code>onFind.pause()</code> to temporarily stop detection and <code>onFind.resume()</code> to restart it.
626
+ Elements added while paused are detected on resume.
627
+ </p>
628
+
629
+ <div class="flex gap-2">
630
+ <button data-testid="pause-add" @click=\${addItem} class="btu-button btu-button-primary btu-button-sm">
631
+ Add Item
632
+ </button>
633
+ <button data-testid="toggle-pause" @click=\${togglePause} class="btu-button btu-button-warning btu-button-sm">
634
+ Pause
635
+ </button>
636
+ </div>
637
+
638
+ <div id="\${instanceId}-container" class="min-h-32 space-y-2 rounded border-2 border-gray-300 bg-gray-50 p-4">
639
+ <div class="text-sm text-gray-500">Items will appear here...</div>
640
+ </div>
641
+
642
+ <div>
643
+ <div class="text-xs text-gray-500">Items detected by onFind</div>
644
+ <div
645
+ data-testid="pause-found-count"
646
+ id="\${instanceId}-found-count"
647
+ class="text-primary-600 text-2xl font-bold"
648
+ >
649
+ 0
650
+ </div>
651
+ </div>
652
+ </div>
653
+ \`;
654
+ },
655
+ parameters: {
656
+ docs: {
657
+ description: {
658
+ story: \`Demonstrates \\\`onFind.pause()\\\` and \\\`onFind.resume()\\\`. While paused, DOM mutations are queued and processed when resumed.\`
659
+ }
660
+ }
661
+ },
662
+ play: async ({
663
+ canvasElement,
664
+ step
665
+ }) => {
666
+ const canvas = within(canvasElement);
667
+ await step('Normal detection before pause', async () => {
668
+ await userEvent.click(canvas.getByTestId('pause-add'));
669
+ await waitFor(() => expect(canvas.getByTestId('pause-found-count')).toHaveTextContent('1'));
670
+ });
671
+ await step('Pause stops detection', async () => {
672
+ await userEvent.click(canvas.getByTestId('toggle-pause'));
673
+ await userEvent.click(canvas.getByTestId('pause-add'));
674
+ // Wait enough time for normal detection to fire if it were going to
675
+ await new Promise(resolve => setTimeout(resolve, 200));
676
+ expect(canvas.getByTestId('pause-found-count')).toHaveTextContent('1');
677
+ });
678
+ await step('Resume triggers pending detection', async () => {
679
+ await userEvent.click(canvas.getByTestId('toggle-pause'));
680
+ // resume() does setTimeout(triggerCallbacks, 100) then rAF — needs generous timeout
681
+ await waitFor(() => expect(canvas.getByTestId('pause-found-count')).toHaveTextContent('2'), {
682
+ timeout: 2000
683
+ });
684
+ });
685
+ }
686
+ }`,...w.parameters?.docs?.source}}};I.parameters={...I.parameters,docs:{...I.parameters?.docs,source:{originalSource:`{
687
+ render: () => {
688
+ const onFindA = onFind;
689
+ const selectorA = '.singleton-item-a';
690
+ const selectorB = '.singleton-item-b';
691
+ let countA = 0;
692
+ let countB = 0;
693
+ const updateCounts = () => {
694
+ const elA = document.getElementById('singleton-count-a');
695
+ const elB = document.getElementById('singleton-count-b');
696
+ if (elA) elA.textContent = String(countA);
697
+ if (elB) elB.textContent = String(countB);
698
+ };
699
+
700
+ // Bundle A sets up the ignore list — Bundle B should respect it
701
+ onFindA.ignore('.singleton-ignored-zone');
702
+
703
+ // Bundle A registers selectorA
704
+ onFindA(selectorA, (element: HTMLElement) => {
705
+ countA++;
706
+ updateCounts();
707
+ element.classList.add('ring-2', 'ring-blue-500');
708
+ });
709
+
710
+ // Bundle B (separate module scope, own _state cache) registers selectorB
711
+ onFindB(selectorB, (element: HTMLElement) => {
712
+ countB++;
713
+ updateCounts();
714
+ element.classList.add('ring-2', 'ring-purple-500');
715
+ });
716
+ const addItem = (selector: string, label: string, zoneId: string) => {
717
+ const container = document.getElementById(zoneId);
718
+ if (container) {
719
+ const item = document.createElement('div');
720
+ item.className = \`\${selector.slice(1)} rounded border bg-white p-4\`;
721
+ item.textContent = label;
722
+ container.appendChild(item);
723
+ }
724
+ };
725
+ let paused = false;
726
+
727
+ // Pause/resume via Bundle A — should also affect Bundle B's callbacks
728
+ const togglePause = () => {
729
+ paused = !paused;
730
+ if (paused) {
731
+ onFindA.pause();
732
+ } else {
733
+ onFindA.resume();
734
+ }
735
+ const btn = document.querySelector('[data-testid="singleton-toggle-pause"]') as HTMLButtonElement;
736
+ if (btn) {
737
+ btn.textContent = paused ? 'Resume' : 'Pause';
738
+ btn.className = paused ? 'btu-button btu-button-success btu-button-sm' : 'btu-button btu-button-warning btu-button-sm';
739
+ }
740
+ };
741
+ const addIgnored = () => {
742
+ addItem(selectorA, 'Ignored A', 'singleton-ignored');
743
+ addItem(selectorB, 'Ignored B', 'singleton-ignored');
744
+ };
745
+ return html\`
746
+ <div class="space-y-4">
747
+ <p class="text-base">
748
+ Two separate ES module instances of <code>onFind</code> (each with its own <code>_state</code> cache) share
749
+ one observer, ignore list, and pause/resume state via <code>globalThis[Symbol.for()]</code>.
750
+ </p>
751
+
752
+ <div class="flex items-center gap-4">
753
+ <button
754
+ data-testid="singleton-toggle-pause"
755
+ @click=\${togglePause}
756
+ class="btu-button btu-button-warning btu-button-sm"
757
+ >
758
+ Pause
759
+ </button>
760
+ <div class="flex items-center gap-1 text-sm text-blue-600">
761
+ Bundle A:
762
+ <span data-testid="singleton-count-a" id="singleton-count-a" class="font-bold">0</span>
763
+ items found
764
+ </div>
765
+ <div class="flex items-center gap-1 text-sm text-purple-600">
766
+ Bundle B:
767
+ <span data-testid="singleton-count-b" id="singleton-count-b" class="font-bold">0</span>
768
+ items found
769
+ </div>
770
+ </div>
771
+
772
+ <div class="grid grid-cols-3 gap-4">
773
+ <div>
774
+ <div class="mb-2 text-xs font-semibold text-blue-600">Bundle A zone</div>
775
+ <div class="mb-2 flex gap-1">
776
+ <button
777
+ data-testid="singleton-add-a"
778
+ @click=\${() => addItem(selectorA, 'Bundle A item', 'singleton-zone-a')}
779
+ class="btu-button btu-button-primary btu-button-sm"
780
+ >
781
+ Add A
782
+ </button>
783
+ <button
784
+ data-testid="singleton-zone-a-add-b"
785
+ @click=\${() => addItem(selectorB, 'Bundle B item', 'singleton-zone-a')}
786
+ class="btu-button btu-button-purple btu-button-sm"
787
+ >
788
+ Add B
789
+ </button>
790
+ </div>
791
+ <div id="singleton-zone-a" class="min-h-24 space-y-2 rounded border-2 border-blue-300 bg-blue-50 p-4">
792
+ <div class="text-sm text-gray-400">A items here</div>
793
+ </div>
794
+ </div>
795
+ <div>
796
+ <div class="mb-2 text-xs font-semibold text-purple-600">Bundle B zone</div>
797
+ <div class="mb-2 flex gap-1">
798
+ <button
799
+ data-testid="singleton-zone-b-add-a"
800
+ @click=\${() => addItem(selectorA, 'Bundle A item', 'singleton-zone-b')}
801
+ class="btu-button btu-button-primary btu-button-sm"
802
+ >
803
+ Add A
804
+ </button>
805
+ <button
806
+ data-testid="singleton-add-b"
807
+ @click=\${() => addItem(selectorB, 'Bundle B item', 'singleton-zone-b')}
808
+ class="btu-button btu-button-purple btu-button-sm"
809
+ >
810
+ Add B
811
+ </button>
812
+ </div>
813
+ <div id="singleton-zone-b" class="min-h-24 space-y-2 rounded border-2 border-purple-300 bg-purple-50 p-4">
814
+ <div class="text-sm text-gray-400">B items here</div>
815
+ </div>
816
+ </div>
817
+ <div>
818
+ <div class="mb-2 text-xs font-semibold text-red-600">Ignored zone</div>
819
+ <div class="mb-2 flex gap-1">
820
+ <button
821
+ data-testid="singleton-add-ignored"
822
+ @click=\${addIgnored}
823
+ class="btu-button btu-button-gray btu-button-sm"
824
+ >
825
+ Add A & B
826
+ </button>
827
+ </div>
828
+ <div
829
+ id="singleton-ignored"
830
+ class="singleton-ignored-zone min-h-24 space-y-2 rounded border-2 border-red-300 bg-red-50 p-4"
831
+ >
832
+ <div class="text-sm text-gray-400">Items ignored here</div>
833
+ </div>
834
+ </div>
835
+ </div>
836
+ </div>
837
+ \`;
838
+ },
839
+ parameters: {
840
+ docs: {
841
+ description: {
842
+ story: \`Two independent module instances of \\\`onFind\\\` (simulating separate bundles) share a single MutationObserver, ignore list, and pause/resume state through the \\\`globalThis[Symbol.for()]\\\` singleton pattern.\`
843
+ }
844
+ }
845
+ },
846
+ play: async ({
847
+ canvasElement,
848
+ step
849
+ }) => {
850
+ const canvas = within(canvasElement);
851
+ await step('Shared observer: Bundle A detects A items, Bundle B detects B items', async () => {
852
+ await userEvent.click(canvas.getByTestId('singleton-add-a'));
853
+ await waitFor(() => expect(canvas.getByTestId('singleton-count-a')).toHaveTextContent('1'));
854
+ await userEvent.click(canvas.getByTestId('singleton-add-b'));
855
+ await waitFor(() => expect(canvas.getByTestId('singleton-count-b')).toHaveTextContent('1'));
856
+ });
857
+ await step('Cross-zone detection fires correct callback', async () => {
858
+ // Add B-selector item to zone A — should increment B count, not A
859
+ await userEvent.click(canvas.getByTestId('singleton-zone-a-add-b'));
860
+ await waitFor(() => expect(canvas.getByTestId('singleton-count-b')).toHaveTextContent('2'));
861
+ expect(canvas.getByTestId('singleton-count-a')).toHaveTextContent('1');
862
+
863
+ // Add A-selector item to zone B — should increment A count, not B
864
+ await userEvent.click(canvas.getByTestId('singleton-zone-b-add-a'));
865
+ await waitFor(() => expect(canvas.getByTestId('singleton-count-a')).toHaveTextContent('2'));
866
+ expect(canvas.getByTestId('singleton-count-b')).toHaveTextContent('2');
867
+ });
868
+ await step('Shared ignore list: Bundle A ignore() blocks Bundle B callbacks', async () => {
869
+ const countABefore = canvas.getByTestId('singleton-count-a').textContent;
870
+ const countBBefore = canvas.getByTestId('singleton-count-b').textContent;
871
+ await userEvent.click(canvas.getByTestId('singleton-add-ignored'));
872
+ await new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve)));
873
+ expect(canvas.getByTestId('singleton-count-a')).toHaveTextContent(countABefore!);
874
+ expect(canvas.getByTestId('singleton-count-b')).toHaveTextContent(countBBefore!);
875
+ });
876
+ await step('Ignore lists are merged and deduped across bundles', async () => {
877
+ const state = (globalThis as any)[Symbol.for('brightspot.onFind')];
878
+ const before = state.blacklist.length;
879
+
880
+ // Bundle B ignores the same selector Bundle A already added in render()
881
+ onFindB.ignore('.singleton-ignored-zone');
882
+ expect(state.blacklist.length).toBe(before); // no duplicate
883
+
884
+ // Bundle B adds a new selector — it merges into the shared list
885
+ onFindB.ignore('.bundle-b-ignored');
886
+ expect(state.blacklist).toContain('.singleton-ignored-zone');
887
+ expect(state.blacklist).toContain('.bundle-b-ignored');
888
+ expect(state.blacklist.length).toBe(before + 1);
889
+ });
890
+ await step('Shared pause/resume: Bundle A pause() stops Bundle B detection', async () => {
891
+ await userEvent.click(canvas.getByTestId('singleton-toggle-pause'));
892
+ await userEvent.click(canvas.getByTestId('singleton-add-a'));
893
+ await userEvent.click(canvas.getByTestId('singleton-add-b'));
894
+ await new Promise(resolve => setTimeout(resolve, 200));
895
+
896
+ // Still at previous counts while paused
897
+ expect(canvas.getByTestId('singleton-count-a')).toHaveTextContent('2');
898
+ expect(canvas.getByTestId('singleton-count-b')).toHaveTextContent('2');
899
+ await userEvent.click(canvas.getByTestId('singleton-toggle-pause'));
900
+ await waitFor(() => expect(canvas.getByTestId('singleton-count-a')).toHaveTextContent('3'), {
901
+ timeout: 2000
902
+ });
903
+ await waitFor(() => expect(canvas.getByTestId('singleton-count-b')).toHaveTextContent('3'), {
904
+ timeout: 2000
905
+ });
906
+ });
907
+ await step('No index collision on data-ofc attributes', async () => {
908
+ const allDetected = canvasElement.querySelectorAll('.singleton-item-a, .singleton-item-b');
909
+ const indices = new Set<string>();
910
+ allDetected.forEach(el => {
911
+ el.getAttributeNames().filter(name => name.startsWith('data-ofc')).forEach(name => indices.add(name));
912
+ });
913
+ expect(indices.size).toBeGreaterThanOrEqual(2);
914
+ });
915
+ await step('Singleton state: both bundles share the same globalThis object', async () => {
916
+ const state = (globalThis as any)[Symbol.for('brightspot.onFind')];
917
+ expect(state).toBeDefined();
918
+ expect(state.callbacks).toBeDefined();
919
+ expect(Array.isArray(state.callbacks)).toBe(true);
920
+ expect(state.initialized).toBe(true);
921
+
922
+ // Function refs are different (proving separate module scopes) but state is shared
923
+ expect(onFind.pause).not.toBe(onFindB.pause);
924
+ expect(onFind.resume).not.toBe(onFindB.resume);
925
+ });
926
+ }
927
+ }`,...I.parameters?.docs?.source}}};T.parameters={...T.parameters,docs:{...T.parameters?.docs,source:{originalSource:`{
928
+ render: () => html\`
929
+ <div class="space-y-4 text-sm">
930
+ <div>
931
+ <h3 class="mb-2 font-bold">Basic Usage</h3>
932
+ <pre
933
+ class="overflow-x-auto rounded bg-gray-900 p-4 text-gray-100"
934
+ ><code>import onFind from '@brightspot/ui/util/onFind.js'
935
+
936
+ // Watch for elements globally
937
+ onFind('.my-component', (element) => {
938
+ console.log('Found element:', element)
939
+ // Initialize component
940
+ })
941
+
942
+ // Watch within a specific root element
943
+ const container = document.querySelector('#container')
944
+ onFind(container, '.my-widget', (element) => {
945
+ // Initialize widget
946
+ })</code></pre>
947
+ </div>
948
+
949
+ <div>
950
+ <h3 class="mb-2 font-bold">Multiple Selectors</h3>
951
+ <pre class="overflow-x-auto rounded bg-gray-900 p-4 text-gray-100"><code>// Watch for multiple selectors at once
952
+ onFind(['.button', '.input', '.select'], (element) => {
953
+ if (element.matches('.button')) {
954
+ // Handle button
955
+ } else if (element.matches('.input')) {
956
+ // Handle input
957
+ }
958
+ })</code></pre>
959
+ </div>
960
+
961
+ <div>
962
+ <h3 class="mb-2 font-bold">Pause and Resume</h3>
963
+ <pre class="overflow-x-auto rounded bg-gray-900 p-4 text-gray-100"><code>// Pause observations temporarily
964
+ onFind.pause()
965
+
966
+ // Make bulk DOM changes without triggering callbacks
967
+ // ...
968
+
969
+ // Resume observations
970
+ onFind.resume()</code></pre>
971
+ </div>
972
+
973
+ <div>
974
+ <h3 class="mb-2 font-bold">Ignore Selectors</h3>
975
+ <pre
976
+ class="overflow-x-auto rounded bg-gray-900 p-4 text-gray-100"
977
+ ><code>// Exclude elements inside matching containers from all callbacks.
978
+ // Calls are additive and shared across bundles.
979
+ onFind.ignore('.ProseMirror', '.RichTextEditor')
980
+
981
+ // Elements inside ignored containers will not trigger callbacks,
982
+ // even if they match a registered selector.</code></pre>
983
+ </div>
984
+
985
+ <div>
986
+ <h3 class="mb-2 font-bold">Common Use Cases</h3>
987
+ <ul class="list-inside list-disc space-y-1 text-gray-700">
988
+ <li>Initialize components when they appear in the DOM</li>
989
+ <li>Attach event listeners to dynamically created elements</li>
990
+ <li>Enhance server-rendered content progressively</li>
991
+ <li>Detect and respond to attribute changes</li>
992
+ <li>Apply transformations to newly added content</li>
993
+ </ul>
994
+ </div>
995
+
996
+ <div>
997
+ <h3 class="mb-2 font-bold">Parameters</h3>
998
+ <ul class="space-y-2">
999
+ <li>
1000
+ <code class="rounded bg-gray-200 px-1">root</code> (ParentNode, optional): Root element to observe. Defaults
1001
+ to document.
1002
+ </li>
1003
+ <li>
1004
+ <code class="rounded bg-gray-200 px-1">selectors</code> (string | string[]): CSS selector(s) to watch for
1005
+ </li>
1006
+ <li>
1007
+ <code class="rounded bg-gray-200 px-1">fn</code> (function): Callback executed when matching elements are
1008
+ found
1009
+ </li>
1010
+ </ul>
1011
+ </div>
1012
+
1013
+ <div>
1014
+ <h3 class="mb-2 font-bold">Static Methods</h3>
1015
+ <table class="w-full text-left text-sm">
1016
+ <thead>
1017
+ <tr class="border-b">
1018
+ <th class="py-2 pr-4">Method</th>
1019
+ <th class="py-2 pr-4">Signature</th>
1020
+ <th class="py-2">Description</th>
1021
+ </tr>
1022
+ </thead>
1023
+ <tbody>
1024
+ <tr class="border-b">
1025
+ <td class="py-2 pr-4"><code class="rounded bg-gray-200 px-1">onFind.ignore</code></td>
1026
+ <td class="py-2 pr-4"><code class="text-xs">(...selectors: string[]) =&gt; void</code></td>
1027
+ <td class="py-2">
1028
+ Exclude elements inside matching containers from all callbacks. Calls are additive and shared across
1029
+ bundles.
1030
+ </td>
1031
+ </tr>
1032
+ <tr class="border-b">
1033
+ <td class="py-2 pr-4"><code class="rounded bg-gray-200 px-1">onFind.pause</code></td>
1034
+ <td class="py-2 pr-4"><code class="text-xs">() =&gt; void</code></td>
1035
+ <td class="py-2">Pause mutation processing. Mutations that occur while paused trigger on resume.</td>
1036
+ </tr>
1037
+ <tr class="border-b">
1038
+ <td class="py-2 pr-4"><code class="rounded bg-gray-200 px-1">onFind.resume</code></td>
1039
+ <td class="py-2 pr-4"><code class="text-xs">() =&gt; void</code></td>
1040
+ <td class="py-2">Resume mutation processing after a pause.</td>
1041
+ </tr>
1042
+ <tr>
1043
+ <td class="py-2 pr-4"><code class="rounded bg-gray-200 px-1">onFind.triggerCallbacks</code></td>
1044
+ <td class="py-2 pr-4"><code class="text-xs">(mutations?: MutationRecord[]) =&gt; void</code></td>
1045
+ <td class="py-2">Manually trigger callback evaluation for all registered selectors.</td>
1046
+ </tr>
1047
+ </tbody>
1048
+ </table>
1049
+ </div>
1050
+
1051
+ <div>
1052
+ <h3 class="mb-2 font-bold">How It Works</h3>
1053
+ <p class="text-gray-700">
1054
+ onFind uses MutationObserver to efficiently watch for DOM changes. Each callback is executed only once per
1055
+ element using data attributes to track already-processed elements. The observer watches for childList
1056
+ mutations and specific attribute changes. Use <code class="rounded bg-gray-200 px-1">onFind.ignore()</code>
1057
+ to exclude containers (e.g., rich-text editors) from triggering callbacks.
1058
+ </p>
1059
+ </div>
1060
+ </div>
1061
+ \`,
1062
+ parameters: {
1063
+ docs: {
1064
+ description: {
1065
+ story: \`Code examples and documentation for using the onFind utility.\`
1066
+ }
1067
+ }
1068
+ }
1069
+ }`,...T.parameters?.docs?.source}}};const W=["Interactive","Ignore","PauseResume","Singleton","UsageExample"];export{B as Ignore,f as Interactive,w as PauseResume,I as Singleton,T as UsageExample,W as __namedExportsOrder,U as default};