@yewdiel21/motion-kit 0.0.0-stage → 0.9.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 (391) hide show
  1. package/NOTICE +38 -0
  2. package/README.md +80 -2
  3. package/bin/motion-kit.mjs +242 -0
  4. package/css.d.ts +2 -0
  5. package/dist/auto/index.d.ts +63 -0
  6. package/dist/auto/index.js +207 -0
  7. package/dist/auto/index.js.map +1 -0
  8. package/dist/chunk-2BQGFOA7.js +177 -0
  9. package/dist/chunk-2BQGFOA7.js.map +1 -0
  10. package/dist/chunk-34XYOXHG.js +2374 -0
  11. package/dist/chunk-34XYOXHG.js.map +1 -0
  12. package/dist/chunk-6OJAP6ZL.js +832 -0
  13. package/dist/chunk-6OJAP6ZL.js.map +1 -0
  14. package/dist/chunk-ABNRDDO7.js +28 -0
  15. package/dist/chunk-ABNRDDO7.js.map +1 -0
  16. package/dist/chunk-FS2HJVVY.js +3320 -0
  17. package/dist/chunk-FS2HJVVY.js.map +1 -0
  18. package/dist/chunk-J27KH2A2.js +157 -0
  19. package/dist/chunk-J27KH2A2.js.map +1 -0
  20. package/dist/chunk-KLY5BRRI.js +18 -0
  21. package/dist/chunk-KLY5BRRI.js.map +1 -0
  22. package/dist/chunk-Q4LVFNLN.js +1617 -0
  23. package/dist/chunk-Q4LVFNLN.js.map +1 -0
  24. package/dist/chunk-RFDIZYFX.js +304 -0
  25. package/dist/chunk-RFDIZYFX.js.map +1 -0
  26. package/dist/chunk-TGV27ZWM.js +17 -0
  27. package/dist/chunk-TGV27ZWM.js.map +1 -0
  28. package/dist/confetti-BSX1CLLV.d.ts +41 -0
  29. package/dist/css/components/chat-background.css +87 -0
  30. package/dist/css/components/chat.css +237 -0
  31. package/dist/css/components/cursor.css +178 -0
  32. package/dist/css/components/dialog.css +259 -0
  33. package/dist/css/components/island.css +301 -0
  34. package/dist/css/components/menu.css +105 -0
  35. package/dist/css/components/nav.css +1054 -0
  36. package/dist/css/components/steps.css +50 -0
  37. package/dist/css/components/theme-picker.css +115 -0
  38. package/dist/css/components/toast.css +115 -0
  39. package/dist/css/controls.css +950 -0
  40. package/dist/css/effects.css +587 -0
  41. package/dist/css/fonts.css +3 -0
  42. package/dist/css/themes/black-green.css +126 -0
  43. package/dist/css/themes/black.css +126 -0
  44. package/dist/css/themes/blue.css +126 -0
  45. package/dist/css/themes/brown.css +126 -0
  46. package/dist/css/themes/cold-blue.css +126 -0
  47. package/dist/css/themes/funny.css +126 -0
  48. package/dist/css/themes/gray.css +126 -0
  49. package/dist/css/themes/green.css +126 -0
  50. package/dist/css/themes/modern-1.css +126 -0
  51. package/dist/css/themes/neogreen.css +126 -0
  52. package/dist/css/themes/new.css +126 -0
  53. package/dist/css/themes/new2.css +126 -0
  54. package/dist/css/themes/oled.css +126 -0
  55. package/dist/css/themes/orange.css +126 -0
  56. package/dist/css/themes/pastel-pink.css +126 -0
  57. package/dist/css/themes/pink.css +126 -0
  58. package/dist/css/themes/purple.css +126 -0
  59. package/dist/css/themes/red.css +126 -0
  60. package/dist/css/themes/super-blue.css +126 -0
  61. package/dist/css/themes.css +2431 -0
  62. package/dist/css/tokens.css +452 -0
  63. package/dist/elements/index.d.ts +183 -0
  64. package/dist/elements/index.js +472 -0
  65. package/dist/elements/index.js.map +1 -0
  66. package/dist/engine-CUr9QijM.d.ts +46 -0
  67. package/dist/gl/index.d.ts +61 -0
  68. package/dist/gl/index.js +18 -0
  69. package/dist/gl/index.js.map +1 -0
  70. package/dist/index.css +5296 -0
  71. package/dist/index.d.ts +440 -0
  72. package/dist/index.js +390 -0
  73. package/dist/index.js.map +1 -0
  74. package/dist/react/index.d.ts +476 -0
  75. package/dist/react/index.js +969 -0
  76. package/dist/react/index.js.map +1 -0
  77. package/dist/reduced-BM4Ls0g5.d.ts +566 -0
  78. package/dist/reveal-WFUWDrei.d.ts +416 -0
  79. package/dist/store-Bqy_obnD.d.ts +195 -0
  80. package/dist/theme/generate.d.ts +18 -0
  81. package/dist/theme/generate.js +64 -0
  82. package/dist/theme/generate.js.map +1 -0
  83. package/dist/theme/index.d.ts +17 -0
  84. package/dist/theme/index.js +32 -0
  85. package/dist/theme/index.js.map +1 -0
  86. package/dist/theme-BFMoCzi6.d.ts +158 -0
  87. package/dist/types-BkS_raL8.d.ts +7 -0
  88. package/package.json +114 -3
  89. package/registry.json +469 -0
  90. package/skills/motion-kit/SKILL.md +189 -0
  91. package/skills/motion-kit/reference/components.md +86 -0
  92. package/skills/motion-kit/reference/design.md +92 -0
  93. package/skills/motion-kit/reference/recipes.md +172 -0
  94. package/src/auto/auto-init.ts +151 -0
  95. package/src/auto/index.ts +3 -0
  96. package/src/auto/parse-options.ts +68 -0
  97. package/src/components/chat/README.md +90 -0
  98. package/src/components/chat/chat.css +236 -0
  99. package/src/components/chat/element.ts +98 -0
  100. package/src/components/chat/engine.ts +295 -0
  101. package/src/components/chat/react.tsx +196 -0
  102. package/src/components/chat-background/README.md +50 -0
  103. package/src/components/chat-background/audio.ts +56 -0
  104. package/src/components/chat-background/chat-background.css +86 -0
  105. package/src/components/chat-background/element.ts +31 -0
  106. package/src/components/chat-background/engine.ts +108 -0
  107. package/src/components/chat-background/react.tsx +36 -0
  108. package/src/components/cursor/README.md +53 -0
  109. package/src/components/cursor/cursor.css +177 -0
  110. package/src/components/cursor/element.ts +23 -0
  111. package/src/components/cursor/engine.ts +254 -0
  112. package/src/components/cursor/react.tsx +26 -0
  113. package/src/components/dialog/README.md +106 -0
  114. package/src/components/dialog/dialog.css +258 -0
  115. package/src/components/dialog/element.ts +118 -0
  116. package/src/components/dialog/engine.ts +432 -0
  117. package/src/components/dialog/react.tsx +200 -0
  118. package/src/components/icon.tsx +10 -0
  119. package/src/components/icons.ts +24 -0
  120. package/src/components/island/README.md +91 -0
  121. package/src/components/island/element.ts +45 -0
  122. package/src/components/island/engine.ts +200 -0
  123. package/src/components/island/island.css +300 -0
  124. package/src/components/island/react.tsx +115 -0
  125. package/src/components/menu/README.md +67 -0
  126. package/src/components/menu/element.ts +55 -0
  127. package/src/components/menu/engine.ts +142 -0
  128. package/src/components/menu/menu.css +104 -0
  129. package/src/components/menu/react.tsx +82 -0
  130. package/src/components/nav/README.md +85 -0
  131. package/src/components/nav/element.ts +39 -0
  132. package/src/components/nav/engine.ts +164 -0
  133. package/src/components/nav/nav.css +1053 -0
  134. package/src/components/nav/react.tsx +90 -0
  135. package/src/components/steps/README.md +70 -0
  136. package/src/components/steps/element.ts +43 -0
  137. package/src/components/steps/engine.ts +185 -0
  138. package/src/components/steps/react.tsx +91 -0
  139. package/src/components/steps/steps.css +49 -0
  140. package/src/components/theme-picker/README.md +57 -0
  141. package/src/components/theme-picker/element.ts +20 -0
  142. package/src/components/theme-picker/engine.ts +91 -0
  143. package/src/components/theme-picker/react.tsx +31 -0
  144. package/src/components/theme-picker/theme-picker.css +114 -0
  145. package/src/components/toast/README.md +60 -0
  146. package/src/components/toast/element.ts +25 -0
  147. package/src/components/toast/engine.ts +483 -0
  148. package/src/components/toast/react.tsx +22 -0
  149. package/src/components/toast/store.ts +122 -0
  150. package/src/components/toast/toast.css +114 -0
  151. package/src/core/channels.ts +165 -0
  152. package/src/core/curves.ts +35 -0
  153. package/src/core/frame.ts +12 -0
  154. package/src/core/gsap.ts +65 -0
  155. package/src/core/loop.ts +44 -0
  156. package/src/core/math.ts +9 -0
  157. package/src/core/physics-spring.ts +59 -0
  158. package/src/core/pointer.ts +63 -0
  159. package/src/core/reduced.ts +21 -0
  160. package/src/core/scope.ts +70 -0
  161. package/src/core/scroll-track.ts +106 -0
  162. package/src/core/split.ts +68 -0
  163. package/src/core/spring.ts +22 -0
  164. package/src/core/styles.ts +31 -0
  165. package/src/core/timing.ts +204 -0
  166. package/src/core/types.ts +20 -0
  167. package/src/core/visible.ts +19 -0
  168. package/src/css/base.css +142 -0
  169. package/src/css/controls/README.md +122 -0
  170. package/src/css/controls/burger.css +67 -0
  171. package/src/css/controls/button.css +226 -0
  172. package/src/css/controls/checkbox.css +53 -0
  173. package/src/css/controls/chip.css +85 -0
  174. package/src/css/controls/field.css +124 -0
  175. package/src/css/controls/glass.css +220 -0
  176. package/src/css/controls/icon.css +26 -0
  177. package/src/css/controls/segmented.css +60 -0
  178. package/src/css/controls/switch.css +72 -0
  179. package/src/css/default-theme.css +123 -0
  180. package/src/css/effects/README.md +134 -0
  181. package/src/css/effects/anim.css +224 -0
  182. package/src/css/effects/fade.css +15 -0
  183. package/src/css/effects/float.css +64 -0
  184. package/src/css/effects/frost.css +62 -0
  185. package/src/css/effects/grain.css +58 -0
  186. package/src/css/effects/loading.css +60 -0
  187. package/src/css/effects/success.css +22 -0
  188. package/src/css/effects/text.css +67 -0
  189. package/src/css/fonts.css +3 -0
  190. package/src/css/states.css +51 -0
  191. package/src/css/themes/black-green.css +126 -0
  192. package/src/css/themes/black.css +126 -0
  193. package/src/css/themes/blue.css +126 -0
  194. package/src/css/themes/brown.css +126 -0
  195. package/src/css/themes/cold-blue.css +126 -0
  196. package/src/css/themes/funny.css +126 -0
  197. package/src/css/themes/gray.css +126 -0
  198. package/src/css/themes/green.css +126 -0
  199. package/src/css/themes/modern-1.css +126 -0
  200. package/src/css/themes/neogreen.css +126 -0
  201. package/src/css/themes/new.css +126 -0
  202. package/src/css/themes/new2.css +126 -0
  203. package/src/css/themes/oled.css +126 -0
  204. package/src/css/themes/orange.css +126 -0
  205. package/src/css/themes/pastel-pink.css +126 -0
  206. package/src/css/themes/pink.css +126 -0
  207. package/src/css/themes/purple.css +126 -0
  208. package/src/css/themes/red.css +126 -0
  209. package/src/css/themes/super-blue.css +126 -0
  210. package/src/css/tokens.css +182 -0
  211. package/src/elements/base.ts +76 -0
  212. package/src/elements/index.ts +48 -0
  213. package/src/gl/index.ts +15 -0
  214. package/src/gl/liquid-blend/README.md +23 -0
  215. package/src/gl/liquid-blend/create-liquid-blend.ts +7 -0
  216. package/src/gl/liquid-blend/shader.ts +9 -0
  217. package/src/gl/matcap/README.md +23 -0
  218. package/src/gl/matcap/create-matcap.ts +7 -0
  219. package/src/gl/matcap/shader.ts +16 -0
  220. package/src/gl/particle-hero/README.md +41 -0
  221. package/src/gl/particle-hero/create-particle-hero.ts +544 -0
  222. package/src/gl/particle-hero/particle-hero.css +19 -0
  223. package/src/gl/particle-hero/shaders.ts +84 -0
  224. package/src/gl/shader-blur/README.md +23 -0
  225. package/src/gl/shader-blur/create-shader-blur.ts +7 -0
  226. package/src/gl/shader-blur/shader.ts +11 -0
  227. package/src/gl/shader-grain/README.md +23 -0
  228. package/src/gl/shader-grain/create-shader-grain.ts +7 -0
  229. package/src/gl/shader-grain/shader.ts +6 -0
  230. package/src/gl/shared/create-shader-effect.ts +186 -0
  231. package/src/gl/shared/shader.css +3 -0
  232. package/src/gl/shared/shaders.ts +30 -0
  233. package/src/index.ts +177 -0
  234. package/src/motion/anchor.ts +45 -0
  235. package/src/motion/chat.ts +97 -0
  236. package/src/motion/confetti.ts +130 -0
  237. package/src/motion/count.ts +95 -0
  238. package/src/motion/details.ts +70 -0
  239. package/src/motion/drag-dismiss.ts +231 -0
  240. package/src/motion/effects.ts +81 -0
  241. package/src/motion/entrances.ts +27 -0
  242. package/src/motion/floating.ts +389 -0
  243. package/src/motion/flow.ts +115 -0
  244. package/src/motion/indicator.ts +44 -0
  245. package/src/motion/layout.ts +39 -0
  246. package/src/motion/liquid.ts +227 -0
  247. package/src/motion/morph.ts +540 -0
  248. package/src/motion/reveal.ts +112 -0
  249. package/src/motion/sheet.ts +65 -0
  250. package/src/motion/surface.ts +108 -0
  251. package/src/react/animated-number.tsx +30 -0
  252. package/src/react/flip-group.tsx +44 -0
  253. package/src/react/frost.tsx +33 -0
  254. package/src/react/index.ts +80 -0
  255. package/src/react/slot-text.tsx +44 -0
  256. package/src/react/theme-provider.tsx +42 -0
  257. package/src/react/use-card-dock.ts +9 -0
  258. package/src/react/use-cascade.ts +9 -0
  259. package/src/react/use-contrast.ts +9 -0
  260. package/src/react/use-counter.ts +8 -0
  261. package/src/react/use-curved-carousel.ts +10 -0
  262. package/src/react/use-depth-grid.ts +9 -0
  263. package/src/react/use-details.ts +7 -0
  264. package/src/react/use-dock-pill.ts +10 -0
  265. package/src/react/use-drag-ruler.ts +10 -0
  266. package/src/react/use-fill-text.ts +9 -0
  267. package/src/react/use-fit-text.ts +8 -0
  268. package/src/react/use-flow.ts +8 -0
  269. package/src/react/use-frame-zoom.ts +9 -0
  270. package/src/react/use-infinite-reel.ts +8 -0
  271. package/src/react/use-letters.ts +9 -0
  272. package/src/react/use-liquid-blend.ts +8 -0
  273. package/src/react/use-marquee.ts +9 -0
  274. package/src/react/use-mask-text.ts +9 -0
  275. package/src/react/use-matcap.ts +8 -0
  276. package/src/react/use-parallax.ts +9 -0
  277. package/src/react/use-particle-hero.ts +9 -0
  278. package/src/react/use-pile.ts +8 -0
  279. package/src/react/use-pop.ts +8 -0
  280. package/src/react/use-radial-menu.ts +10 -0
  281. package/src/react/use-recipe.ts +38 -0
  282. package/src/react/use-reflow.ts +9 -0
  283. package/src/react/use-repel-bento.ts +8 -0
  284. package/src/react/use-reveal.ts +8 -0
  285. package/src/react/use-rotator.ts +10 -0
  286. package/src/react/use-shader-blur.ts +8 -0
  287. package/src/react/use-shader-grain.ts +8 -0
  288. package/src/react/use-slot-text.ts +17 -0
  289. package/src/react/use-snap-strip.ts +9 -0
  290. package/src/react/use-sortable.ts +14 -0
  291. package/src/react/use-spring-cards.ts +8 -0
  292. package/src/react/use-stretch-footer.ts +20 -0
  293. package/src/react/use-tabs.ts +10 -0
  294. package/src/react/use-text-roll.ts +9 -0
  295. package/src/react/use-ticket-menu.ts +15 -0
  296. package/src/react/use-words.ts +8 -0
  297. package/src/recipes/card-dock/README.md +36 -0
  298. package/src/recipes/card-dock/card-dock.css +178 -0
  299. package/src/recipes/card-dock/create-card-dock.ts +97 -0
  300. package/src/recipes/cascade/README.md +33 -0
  301. package/src/recipes/cascade/create-cascade.ts +111 -0
  302. package/src/recipes/contrast/README.md +29 -0
  303. package/src/recipes/contrast/create-contrast.ts +68 -0
  304. package/src/recipes/counter/README.md +42 -0
  305. package/src/recipes/counter/create-counter.ts +84 -0
  306. package/src/recipes/curved-carousel/README.md +50 -0
  307. package/src/recipes/curved-carousel/carousel-math.ts +20 -0
  308. package/src/recipes/curved-carousel/create-curved-carousel.ts +257 -0
  309. package/src/recipes/curved-carousel/curved-carousel.css +50 -0
  310. package/src/recipes/depth-grid/README.md +30 -0
  311. package/src/recipes/depth-grid/create-depth-grid.ts +120 -0
  312. package/src/recipes/details/README.md +31 -0
  313. package/src/recipes/details/create-details.ts +27 -0
  314. package/src/recipes/dock-pill/README.md +23 -0
  315. package/src/recipes/dock-pill/create-dock-pill.ts +68 -0
  316. package/src/recipes/dock-pill/dock-pill.css +5 -0
  317. package/src/recipes/drag-ruler/README.md +21 -0
  318. package/src/recipes/drag-ruler/create-drag-ruler.ts +95 -0
  319. package/src/recipes/drag-ruler/drag-ruler.css +16 -0
  320. package/src/recipes/fill-text/README.md +27 -0
  321. package/src/recipes/fill-text/create-fill-text.ts +93 -0
  322. package/src/recipes/fit-text/README.md +37 -0
  323. package/src/recipes/fit-text/create-fit-text.ts +55 -0
  324. package/src/recipes/fit-text/fit-text.css +6 -0
  325. package/src/recipes/flow/README.md +46 -0
  326. package/src/recipes/flow/create-flow.ts +55 -0
  327. package/src/recipes/frame-zoom/README.md +29 -0
  328. package/src/recipes/frame-zoom/create-frame-zoom.ts +91 -0
  329. package/src/recipes/infinite-reel/README.md +23 -0
  330. package/src/recipes/infinite-reel/create-infinite-reel.ts +140 -0
  331. package/src/recipes/infinite-reel/infinite-reel.css +8 -0
  332. package/src/recipes/letters/README.md +30 -0
  333. package/src/recipes/letters/create-letters.ts +113 -0
  334. package/src/recipes/marquee/README.md +32 -0
  335. package/src/recipes/marquee/create-marquee.ts +74 -0
  336. package/src/recipes/marquee/marquee.css +41 -0
  337. package/src/recipes/mask-text/README.md +39 -0
  338. package/src/recipes/mask-text/create-mask-text.ts +134 -0
  339. package/src/recipes/parallax/README.md +31 -0
  340. package/src/recipes/parallax/create-parallax.ts +74 -0
  341. package/src/recipes/pile/README.md +48 -0
  342. package/src/recipes/pile/create-pile.ts +50 -0
  343. package/src/recipes/pile/pile-math.ts +27 -0
  344. package/src/recipes/pile/pile.css +63 -0
  345. package/src/recipes/pill/README.md +30 -0
  346. package/src/recipes/pill/pill.css +48 -0
  347. package/src/recipes/pop/README.md +36 -0
  348. package/src/recipes/pop/create-pop.ts +52 -0
  349. package/src/recipes/radial-menu/README.md +44 -0
  350. package/src/recipes/radial-menu/create-radial-menu.ts +96 -0
  351. package/src/recipes/radial-menu/radial-menu.css +45 -0
  352. package/src/recipes/reflow/README.md +47 -0
  353. package/src/recipes/reflow/create-reflow.ts +49 -0
  354. package/src/recipes/repel-bento/README.md +21 -0
  355. package/src/recipes/repel-bento/create-repel-bento.ts +65 -0
  356. package/src/recipes/repel-bento/repel-bento.css +3 -0
  357. package/src/recipes/reveal/README.md +36 -0
  358. package/src/recipes/reveal/create-reveal.ts +48 -0
  359. package/src/recipes/rotator/README.md +21 -0
  360. package/src/recipes/rotator/create-rotator.ts +66 -0
  361. package/src/recipes/rotator/rotator.css +3 -0
  362. package/src/recipes/slot-text/README.md +44 -0
  363. package/src/recipes/slot-text/create-slot-text.ts +244 -0
  364. package/src/recipes/slot-text/slot-text.css +42 -0
  365. package/src/recipes/snap-strip/README.md +31 -0
  366. package/src/recipes/snap-strip/create-snap-strip.ts +133 -0
  367. package/src/recipes/snap-strip/snap-strip.css +23 -0
  368. package/src/recipes/sortable/README.md +88 -0
  369. package/src/recipes/sortable/create-sortable.ts +863 -0
  370. package/src/recipes/sortable/sortable.css +40 -0
  371. package/src/recipes/spring-cards/README.md +48 -0
  372. package/src/recipes/spring-cards/create-spring-cards.ts +140 -0
  373. package/src/recipes/spring-cards/spring-cards.css +23 -0
  374. package/src/recipes/stretch-footer/README.md +44 -0
  375. package/src/recipes/stretch-footer/create-stretch-footer.ts +69 -0
  376. package/src/recipes/stretch-footer/stretch-footer.css +28 -0
  377. package/src/recipes/tabs/README.md +39 -0
  378. package/src/recipes/tabs/create-tabs.ts +91 -0
  379. package/src/recipes/tabs/tabs.css +19 -0
  380. package/src/recipes/text-roll/README.md +35 -0
  381. package/src/recipes/text-roll/create-text-roll.ts +137 -0
  382. package/src/recipes/text-roll/text-roll.css +21 -0
  383. package/src/recipes/ticket-menu/README.md +24 -0
  384. package/src/recipes/ticket-menu/create-ticket-menu.ts +6 -0
  385. package/src/recipes/words/README.md +43 -0
  386. package/src/recipes/words/create-words.ts +114 -0
  387. package/src/theme/css-format.ts +32 -0
  388. package/src/theme/generate.ts +72 -0
  389. package/src/theme/index.ts +15 -0
  390. package/src/theme/presets.ts +79 -0
  391. package/src/theme/theme.ts +119 -0
@@ -0,0 +1,295 @@
1
+ import { gsap, Flip, EASE, registerMotion } from '../../core/gsap';
2
+ import { reduced } from '../../core/reduced';
3
+ import { createScope } from '../../core/scope';
4
+ import { splitWords } from '../../core/split';
5
+ import { TIMING } from '../../core/timing';
6
+ import { inert, warnOnce, type Instance } from '../../core/types';
7
+ import { enterMessages, fitComposer, scrollToLatest } from '../../motion/chat';
8
+ import { flowIn } from '../../motion/flow';
9
+ import { iconSvg } from '../icons';
10
+
11
+ /*
12
+ * Stepbro's PrettyChat: new bubbles grow from their speaker's side while their
13
+ * words rise (enterMessages); the conversation scrolls to the newest one
14
+ * (0.35 s). A "Pensando…" placeholder shows only if the answer takes more than
15
+ * 500 ms, and then morphs (Flip, 0.7 s, no scaling of the text) into the reply.
16
+ * The composer grows with its text (0.18 s, from 64 px); Enter sends.
17
+ * Parts: [data-mk-part] messages, composer, input, mic, send.
18
+ * Messages: [data-mk-message] with data-mk-role="user|assistant|system" and a [data-mk-text].
19
+ */
20
+
21
+ export type ChatRole = 'user' | 'assistant' | 'system';
22
+
23
+ export interface ChatLabels {
24
+ thinking?: string;
25
+ }
26
+
27
+ export interface ChatOptions {
28
+ /** Animate the messages already there when the chat starts (default: no, history stays still). */
29
+ animateExisting?: boolean;
30
+ /** Clear the input after sending (default). */
31
+ clearOnSend?: boolean;
32
+ labels?: ChatLabels;
33
+ onSend?: (text: string) => void;
34
+ }
35
+
36
+ export interface ChatInstance extends Instance {
37
+ /** Append a plain-text message (vanilla convenience); it enters on its own. */
38
+ add(role: ChatRole, text: string): HTMLElement;
39
+ /** Show the thinking placeholder after 500 ms (a fast answer never shows it). */
40
+ pending(): void;
41
+ /** Add the answer: it morphs out of the placeholder if that was showing, else it enters. */
42
+ reply(message: HTMLElement | string): HTMLElement;
43
+ /** A confirmation bar flows in and its words rise. */
44
+ confirm(bar: HTMLElement): void;
45
+ setRecording(on: boolean): void;
46
+ scrollToLatest(): void;
47
+ /** Mark a message as already shown (a framework animates it itself). */
48
+ adopt(message: Element): void;
49
+ readonly messages: HTMLElement;
50
+ }
51
+
52
+ const MESSAGE = '[data-mk-message]';
53
+ /** data-flip-id of the thinking placeholder; a framework's own placeholder must carry it. */
54
+ export const PENDING_ID = 'mk-chat-pending';
55
+ const part = (root: HTMLElement, name: string) => root.querySelector<HTMLElement>(`[data-mk-part="${name}"]`);
56
+
57
+ /** The thinking placeholder's geometry, taken before the DOM changes (for frameworks). */
58
+ export const capturePending = (placeholder: Element | null) => {
59
+ registerMotion();
60
+ return placeholder ? Flip.getState(placeholder) : null;
61
+ };
62
+
63
+ /** Morph a captured placeholder into the reply (after the DOM shows the reply). */
64
+ export function morphPendingToReply(state: ReturnType<typeof Flip.getState> | null, reply: HTMLElement, scroller?: HTMLElement | null) {
65
+ registerMotion();
66
+ if (!state || reduced()) {
67
+ if (!reduced()) enterMessages([reply]);
68
+ return null;
69
+ }
70
+ const text = reply.querySelector<HTMLElement>('[data-mk-text], .message-text');
71
+ if (text) text.style.width = `${text.getBoundingClientRect().width}px`;
72
+ // Flip pairs the reply with the placeholder by this id (the placeholder carries it too).
73
+ reply.setAttribute('data-flip-id', PENDING_ID);
74
+ const words = text && !text.querySelector('table, pre') ? splitWords(text) : null;
75
+ if (words?.parts.length) {
76
+ gsap.fromTo(
77
+ words.parts,
78
+ { y: 16, autoAlpha: 0, filter: 'blur(6px)' },
79
+ {
80
+ y: 0,
81
+ autoAlpha: 1,
82
+ filter: 'blur(0px)',
83
+ duration: TIMING.word,
84
+ ease: 'back.out(1)',
85
+ stagger: TIMING.wordStagger,
86
+ clearProps: 'transform,opacity,visibility,filter',
87
+ onComplete: () => words.revert(),
88
+ },
89
+ );
90
+ }
91
+ return Flip.from(state, {
92
+ targets: reply,
93
+ duration: TIMING.bubble,
94
+ scale: false,
95
+ ease: EASE.signature,
96
+ onUpdate: () => {
97
+ if (scroller) scroller.scrollTop = scroller.scrollHeight;
98
+ },
99
+ onComplete: () => {
100
+ reply.removeAttribute('data-flip-id');
101
+ if (text) text.style.width = '';
102
+ },
103
+ });
104
+ }
105
+
106
+ /** A message bubble element (plain text). */
107
+ export function chatMessage(role: ChatRole, text: string) {
108
+ const message = document.createElement('div');
109
+ message.dataset.mkMessage = '';
110
+ message.dataset.mkRole = role;
111
+ const body = document.createElement('span');
112
+ body.dataset.mkText = '';
113
+ body.textContent = text;
114
+ message.append(body);
115
+ return message;
116
+ }
117
+
118
+ export function createChat(root: HTMLElement | null, options: ChatOptions = {}): ChatInstance {
119
+ const messages = root ? part(root, 'messages') : null;
120
+ if (!root || !messages) {
121
+ warnOnce('chat: needs a [data-mk-part="messages"] container');
122
+ return inert({
123
+ add: () => document.createElement('div'),
124
+ pending() {},
125
+ reply: () => document.createElement('div'),
126
+ confirm() {},
127
+ setRecording() {},
128
+ scrollToLatest() {},
129
+ adopt() {},
130
+ messages: null as unknown as HTMLElement,
131
+ });
132
+ }
133
+ registerMotion();
134
+ const { clearOnSend = true } = options;
135
+ const thinking = options.labels?.thinking ?? 'Pensando...';
136
+ const scope = createScope();
137
+ const composer = part(root, 'composer');
138
+ const input = composer?.querySelector<HTMLTextAreaElement>('[data-mk-part="input"], textarea') ?? null;
139
+ const mic = part(root, 'mic');
140
+ const seen = new WeakSet<Element>();
141
+ let placeholder: HTMLElement | null = null;
142
+ let pendingTimer: ReturnType<typeof setTimeout> | undefined;
143
+ let waiting = false;
144
+
145
+ scope.attr(messages, 'role', 'log');
146
+ scope.attr(messages, 'aria-live', 'polite');
147
+ const existing = [...messages.querySelectorAll<HTMLElement>(MESSAGE)];
148
+ if (options.animateExisting) enterMessages(existing);
149
+ for (const message of existing) seen.add(message);
150
+
151
+ // New bubbles (from the app, a framework or add()) enter on their own.
152
+ const observer = scope.observe(
153
+ new MutationObserver((records) => {
154
+ const fresh: HTMLElement[] = [];
155
+ for (const record of records) {
156
+ for (const node of record.addedNodes) {
157
+ if (!(node instanceof HTMLElement) || !node.matches(MESSAGE) || seen.has(node)) continue;
158
+ seen.add(node);
159
+ fresh.push(node);
160
+ }
161
+ }
162
+ if (!fresh.length) return;
163
+ enterMessages(fresh);
164
+ scrollToLatest(messages);
165
+ }),
166
+ );
167
+ observer.observe(messages, { childList: true });
168
+
169
+ const clearPending = () => {
170
+ clearTimeout(pendingTimer);
171
+ pendingTimer = undefined;
172
+ waiting = false;
173
+ };
174
+
175
+ const add = (role: ChatRole, text: string) => {
176
+ const message = chatMessage(role, text);
177
+ messages.append(message);
178
+ return message;
179
+ };
180
+
181
+ const pending = () => {
182
+ clearPending();
183
+ waiting = true;
184
+ pendingTimer = setTimeout(() => {
185
+ pendingTimer = undefined;
186
+ if (!waiting || placeholder) return;
187
+ placeholder = document.createElement('div');
188
+ placeholder.dataset.mkMessage = '';
189
+ placeholder.dataset.mkRole = 'assistant';
190
+ placeholder.dataset.mkPending = '';
191
+ placeholder.setAttribute('data-flip-id', PENDING_ID);
192
+ placeholder.innerHTML = `<span class="mk-chat__thinking">${iconSvg('sparkle', 18)}<span></span></span>`;
193
+ placeholder.querySelector('span span')!.textContent = thinking;
194
+ seen.add(placeholder);
195
+ messages.append(placeholder);
196
+ enterMessages([placeholder]);
197
+ scrollToLatest(messages);
198
+ }, TIMING.chat.pendingDelayMs);
199
+ };
200
+
201
+ const reply = (content: HTMLElement | string) => {
202
+ const message = typeof content === 'string' ? chatMessage('assistant', content) : content;
203
+ clearPending();
204
+ seen.add(message);
205
+ const was = placeholder;
206
+ placeholder = null;
207
+ if (was?.isConnected) {
208
+ const state = capturePending(was);
209
+ was.replaceWith(message);
210
+ morphPendingToReply(state, message, messages);
211
+ } else {
212
+ if (!message.isConnected) messages.append(message);
213
+ enterMessages([message]);
214
+ scrollToLatest(messages);
215
+ }
216
+ return message;
217
+ };
218
+
219
+ const confirm = (bar: HTMLElement) => {
220
+ if (reduced()) return;
221
+ flowIn(bar);
222
+ const words = splitWords(bar);
223
+ if (!words.parts.length) return;
224
+ gsap.fromTo(
225
+ words.parts,
226
+ { y: 16, autoAlpha: 0, filter: 'blur(6px)' },
227
+ {
228
+ y: 0,
229
+ autoAlpha: 1,
230
+ filter: 'blur(0px)',
231
+ duration: TIMING.word,
232
+ ease: 'back.out(1)',
233
+ stagger: TIMING.chat.confirmWordStagger,
234
+ clearProps: 'transform,opacity,visibility,filter',
235
+ onComplete: () => words.revert(),
236
+ },
237
+ );
238
+ };
239
+
240
+ const send = () => {
241
+ if (!input) return;
242
+ const text = input.value.trim();
243
+ if (!text) return;
244
+ const allowed = root.dispatchEvent(new CustomEvent('mk:send', { bubbles: true, cancelable: true, detail: { text } }));
245
+ if (!allowed) return;
246
+ options.onSend?.(text);
247
+ if (clearOnSend) {
248
+ input.value = '';
249
+ fitComposer(input);
250
+ }
251
+ };
252
+
253
+ if (input) {
254
+ scope.on(input, 'input', () => fitComposer(input));
255
+ scope.on(input, 'keydown', (event) => {
256
+ const key = event as KeyboardEvent;
257
+ if (key.key !== 'Enter' || key.shiftKey || key.isComposing) return;
258
+ event.preventDefault();
259
+ send();
260
+ });
261
+ }
262
+ if (composer) {
263
+ scope.on(composer, 'submit', (event) => {
264
+ event.preventDefault();
265
+ send();
266
+ });
267
+ const sendButton = part(root, 'send');
268
+ if (sendButton && !composer.matches('form')) scope.on(sendButton, 'click', send);
269
+ }
270
+ scope.add(() => {
271
+ clearPending();
272
+ placeholder?.remove();
273
+ placeholder = null;
274
+ });
275
+ if (input) fitComposer(input);
276
+ root.dataset.mkReady = '';
277
+
278
+ return {
279
+ add,
280
+ pending,
281
+ reply,
282
+ confirm,
283
+ setRecording(on: boolean) {
284
+ if (mic) scope.attr(mic, 'data-mk-recording', on ? '' : null);
285
+ },
286
+ scrollToLatest: () => scrollToLatest(messages),
287
+ adopt: (message: Element) => seen.add(message),
288
+ messages,
289
+ destroy() {
290
+ scope.dispose();
291
+ if (input) input.style.removeProperty('height');
292
+ delete root.dataset.mkReady;
293
+ },
294
+ };
295
+ }
@@ -0,0 +1,196 @@
1
+ 'use client';
2
+
3
+ import {
4
+ Component,
5
+ createContext,
6
+ useContext,
7
+ useRef,
8
+ type ContextType,
9
+ type HTMLAttributes,
10
+ type MutableRefObject,
11
+ type ReactNode,
12
+ type TextareaHTMLAttributes,
13
+ } from 'react';
14
+ import { useIsomorphicLayoutEffect, useLatest } from '../../react/use-recipe';
15
+ import { MkIcon } from '../icon';
16
+ import { TIMING } from '../../core/timing';
17
+ import { PENDING_ID, capturePending, createChat, morphPendingToReply, type ChatInstance, type ChatLabels, type ChatRole } from './engine';
18
+
19
+ interface ChatContextValue {
20
+ instance: MutableRefObject<ChatInstance | null>;
21
+ labels: ChatLabels;
22
+ }
23
+
24
+ const ChatContext = createContext<ChatContextValue | null>(null);
25
+
26
+ export interface ChatRootProps extends HTMLAttributes<HTMLDivElement> {
27
+ onSend?: (text: string) => void;
28
+ animateExisting?: boolean;
29
+ labels?: ChatLabels;
30
+ children?: ReactNode;
31
+ }
32
+
33
+ function Root({ onSend, animateExisting, labels = {}, className, children, ...props }: ChatRootProps) {
34
+ const ref = useRef<HTMLDivElement>(null);
35
+ const instance = useRef<ChatInstance | null>(null);
36
+ const send = useLatest(onSend);
37
+ useIsomorphicLayoutEffect(() => {
38
+ const created = createChat(ref.current, { animateExisting, labels, onSend: (text) => send.current?.(text) });
39
+ instance.current = created;
40
+ return () => {
41
+ created.destroy();
42
+ instance.current = null;
43
+ };
44
+ // The engine is created once; callbacks are read through refs.
45
+ // eslint-disable-next-line react-hooks/exhaustive-deps
46
+ }, []);
47
+ return (
48
+ <ChatContext.Provider value={{ instance, labels }}>
49
+ <div ref={ref} className={['mk-chat', className].filter(Boolean).join(' ')} {...props}>
50
+ {children}
51
+ </div>
52
+ </ChatContext.Provider>
53
+ );
54
+ }
55
+
56
+ export interface ChatMessagesProps {
57
+ /** Waiting for an answer: "Pensando…" shows after 500 ms and morphs into the next assistant message. */
58
+ pending?: boolean;
59
+ children?: ReactNode;
60
+ }
61
+
62
+ interface MessagesState {
63
+ showPending: boolean;
64
+ }
65
+
66
+ type Snapshot = ReturnType<typeof capturePending>;
67
+
68
+ /**
69
+ * The message list. A class component: only getSnapshotBeforeUpdate can measure
70
+ * the placeholder before React replaces it with the answer.
71
+ */
72
+ class Messages extends Component<ChatMessagesProps, MessagesState> {
73
+ static contextType = ChatContext;
74
+ declare context: ContextType<typeof ChatContext>;
75
+ state: MessagesState = { showPending: false };
76
+ private list: HTMLDivElement | null = null;
77
+ private timer: ReturnType<typeof setTimeout> | undefined;
78
+
79
+ override componentDidMount() {
80
+ if (this.props.pending) this.wait();
81
+ }
82
+
83
+ override componentDidUpdate(previous: ChatMessagesProps, _state: MessagesState, snapshot: Snapshot) {
84
+ if (this.props.pending && !previous.pending) this.wait();
85
+ if (!this.props.pending && previous.pending) {
86
+ clearTimeout(this.timer);
87
+ if (this.state.showPending) this.setState({ showPending: false });
88
+ }
89
+ if (!snapshot || !this.list) return;
90
+ const answers = [...this.list.querySelectorAll<HTMLElement>('[data-mk-message][data-mk-role="assistant"]:not([data-mk-pending])')];
91
+ const reply = answers[answers.length - 1];
92
+ if (!reply) return;
93
+ this.context?.instance.current?.adopt(reply);
94
+ morphPendingToReply(snapshot, reply, this.list);
95
+ }
96
+
97
+ override getSnapshotBeforeUpdate(previous: ChatMessagesProps): Snapshot {
98
+ // The answer arrives as `pending` turns off: measure the placeholder while it is still there.
99
+ if (!previous.pending || this.props.pending || !this.state.showPending || !this.list) return null;
100
+ return capturePending(this.list.querySelector('[data-mk-pending]'));
101
+ }
102
+
103
+ override componentWillUnmount() {
104
+ clearTimeout(this.timer);
105
+ }
106
+
107
+ private wait() {
108
+ clearTimeout(this.timer);
109
+ this.timer = setTimeout(() => {
110
+ if (this.props.pending) this.setState({ showPending: true });
111
+ }, TIMING.chat.pendingDelayMs);
112
+ }
113
+
114
+ override render() {
115
+ const thinking = this.context?.labels.thinking ?? 'Pensando...';
116
+ return (
117
+ <div
118
+ ref={(node) => {
119
+ this.list = node;
120
+ }}
121
+ data-mk-part="messages"
122
+ >
123
+ {this.props.children}
124
+ {this.props.pending && this.state.showPending ? (
125
+ <div data-mk-message="" data-mk-role="assistant" data-mk-pending="" data-flip-id={PENDING_ID}>
126
+ <span className="mk-chat__thinking">
127
+ <MkIcon name="sparkle" size={18} />
128
+ <span>{thinking}</span>
129
+ </span>
130
+ </div>
131
+ ) : null}
132
+ </div>
133
+ );
134
+ }
135
+ }
136
+
137
+ export interface ChatMessageProps extends HTMLAttributes<HTMLDivElement> {
138
+ role?: ChatRole;
139
+ children?: ReactNode;
140
+ }
141
+
142
+ function Message({ role = 'assistant', children, ...props }: ChatMessageProps) {
143
+ return (
144
+ <div data-mk-message="" data-mk-role={role} {...props}>
145
+ <div data-mk-text="">{children}</div>
146
+ </div>
147
+ );
148
+ }
149
+
150
+ export interface ChatComposerProps extends TextareaHTMLAttributes<HTMLTextAreaElement> {
151
+ /** Extra buttons before send (a microphone…). */
152
+ actions?: ReactNode;
153
+ sendLabel?: string;
154
+ }
155
+
156
+ function Composer({ actions, sendLabel = 'Enviar', placeholder = 'Escribe un mensaje...', ...props }: ChatComposerProps) {
157
+ return (
158
+ <form data-mk-part="composer">
159
+ <textarea data-mk-part="input" rows={1} placeholder={placeholder} aria-label={placeholder} {...props} />
160
+ <div data-mk-part="buttons">
161
+ {actions}
162
+ <button type="submit" data-mk-part="send" aria-label={sendLabel}>
163
+ <MkIcon name="send" size={22} />
164
+ </button>
165
+ </div>
166
+ </form>
167
+ );
168
+ }
169
+
170
+ function Confirm({ children, ...props }: HTMLAttributes<HTMLDivElement>) {
171
+ const ref = useRef<HTMLDivElement>(null);
172
+ const context = useContext(ChatContext);
173
+ useIsomorphicLayoutEffect(() => {
174
+ if (ref.current) context?.instance.current?.confirm(ref.current);
175
+ // Plays once, when the bar appears.
176
+ // eslint-disable-next-line react-hooks/exhaustive-deps
177
+ }, []);
178
+ return (
179
+ <div ref={ref} data-mk-part="confirm" {...props}>
180
+ {children}
181
+ </div>
182
+ );
183
+ }
184
+
185
+ /**
186
+ * Stepbro's chat: <Chat.Root onSend><ChatBackground /><Chat.Messages pending>{messages.map(m =>
187
+ * <Chat.Message role>)}</Chat.Messages><Chat.Composer /></Chat.Root>.
188
+ */
189
+ export const Chat = { Root, Messages, Message, Composer, Confirm };
190
+
191
+ /** The chat engine of the surrounding <Chat.Root> (setRecording, scrollToLatest…). */
192
+ export function useChat() {
193
+ const context = useContext(ChatContext);
194
+ if (!context) throw new Error('useChat() needs a <Chat.Root> above it');
195
+ return context.instance;
196
+ }
@@ -0,0 +1,50 @@
1
+ # chat-background
2
+
3
+ El fondo vivo del chat de Stepbro: dos manchas suaves (primary-container y tertiary-container, degradados radiales de 800 px, saturación 150 %) que orbitan detrás del chat, con grano encima. La energía (0…1, por ejemplo la del micrófono) acelera las órbitas (`10 − 3.5·e` s y `15 − 4.5·e` s), sube su opacidad (`.5 + .15·e`) y las agranda (`(1 + .1·e)·scale`).
4
+
5
+ El estado "ocupado" del onboarding la lleva a energía 1.6 y escala 1.8 en 0.8 s (`power2.out`) y la devuelve en 0.6 s (`power2.inOut`).
6
+
7
+ Origen: Stepbro PrettyBackground (`chat-background`, `background-audio-energy`, `onboarding-busy-background` en el kit). El grano es un SVG `feTurbulence` en lugar del PNG de Stepbro.
8
+
9
+ ## HTML / Astro
10
+
11
+ ```html
12
+ <div class="panel" style="position: relative">
13
+ <mk-chat-background energy="0"></mk-chat-background>
14
+ …
15
+ </div>
16
+ ```
17
+
18
+ `element.setEnergy(0.4)`, `element.busy(true)`. Sin custom element: `createChatBackground(div)`.
19
+
20
+ ## React
21
+
22
+ ```tsx
23
+ <div style={{ position: 'relative' }}>
24
+ <ChatBackground energy={energy} />
25
+ </div>
26
+ ```
27
+
28
+ ## Micrófono
29
+
30
+ ```js
31
+ import { audioEnergy, createChatBackground } from '@yewdiel21/motion-kit';
32
+ const background = createChatBackground(el);
33
+ const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); // tu app pide el permiso
34
+ const meter = audioEnergy(stream, (energy) => background.setEnergy(energy));
35
+ // al terminar: meter.stop()
36
+ ```
37
+
38
+ `audioEnergy` usa FFT 256, suavizado .3, −90…−10 dB, el 40 % grave de las bandas ×3 (máx. 1), ataque .8 y caída .15. Nunca pide el micrófono por su cuenta.
39
+
40
+ ## API
41
+
42
+ `createChatBackground(root, { energy, scale })` → `{ setEnergy, setScale, busy(on), energy, scale, destroy }`; `chatBackgroundOf(el)` devuelve la instancia que corre en un elemento.
43
+
44
+ ## Variables CSS
45
+
46
+ `--mk-chat-blob-1`, `--mk-chat-blob-2` (colores), `--mk-chat-background-opacity`; las escribe el motor: `--mk-chat-orbit-1`, `--mk-chat-orbit-2`, `--mk-chat-blob-opacity`, `--mk-chat-scale`.
47
+
48
+ ## Accesibilidad
49
+
50
+ Decorativo (`aria-hidden`). Con movimiento reducido las manchas no orbitan.
@@ -0,0 +1,56 @@
1
+ /*
2
+ * Microphone energy for the chat background (Stepbro's analyser): FFT 256,
3
+ * smoothing .3, −90…−10 dB; the lowest 40 % of the bins, ×3, clamped to 1, with
4
+ * attack .8 and release .15. The caller passes a MediaStream it already has
5
+ * permission for: this never asks for the microphone.
6
+ */
7
+
8
+ export interface AudioEnergy {
9
+ stop(): void;
10
+ }
11
+
12
+ export function audioEnergy(
13
+ stream: MediaStream,
14
+ onEnergy: (energy: number) => void,
15
+ { fftSize = 256, smoothing = 0.3 }: { fftSize?: number; smoothing?: number } = {},
16
+ ): AudioEnergy {
17
+ const AudioCtor = (globalThis as { AudioContext?: typeof AudioContext }).AudioContext;
18
+ if (!AudioCtor) {
19
+ onEnergy(0);
20
+ return { stop() {} };
21
+ }
22
+ const context = new AudioCtor();
23
+ const analyser = context.createAnalyser();
24
+ analyser.fftSize = fftSize;
25
+ analyser.smoothingTimeConstant = smoothing;
26
+ analyser.minDecibels = -90;
27
+ analyser.maxDecibels = -10;
28
+ const input = context.createMediaStreamSource(stream);
29
+ input.connect(analyser);
30
+ const bins = new Uint8Array(analyser.frequencyBinCount);
31
+ let energy = 0;
32
+ let frame = 0;
33
+ let stopped = false;
34
+ const tick = () => {
35
+ if (stopped) return;
36
+ analyser.getByteFrequencyData(bins);
37
+ const count = Math.ceil(bins.length * 0.4);
38
+ let sum = 0;
39
+ for (let i = 0; i < count; i++) sum += bins[i] ?? 0;
40
+ const target = Math.min(((sum / (count * 255)) * 3), 1);
41
+ energy += (target - energy) * (target > energy ? 0.8 : 0.15);
42
+ onEnergy(energy);
43
+ frame = requestAnimationFrame(tick);
44
+ };
45
+ tick();
46
+ return {
47
+ stop() {
48
+ if (stopped) return;
49
+ stopped = true;
50
+ cancelAnimationFrame(frame);
51
+ input.disconnect();
52
+ void context.close();
53
+ onEnergy(0);
54
+ },
55
+ };
56
+ }
@@ -0,0 +1,86 @@
1
+ /* chat-background — Stepbro's PrettyBackground: two orbiting blobs and a grain, driven by energy. */
2
+ .mk-chat-background,
3
+ mk-chat-background {
4
+ --mk-chat-orbit-1: 10s;
5
+ --mk-chat-orbit-2: 15s;
6
+ --mk-chat-blob-opacity: 0.5;
7
+ --mk-chat-scale: 1;
8
+ --mk-chat-background-opacity: 1;
9
+ --mk-chat-blob-1: var(--md-sys-color-primary-container);
10
+ --mk-chat-blob-2: var(--md-sys-color-tertiary-container);
11
+ position: absolute;
12
+ inset: 0;
13
+ display: block;
14
+ overflow: hidden;
15
+ border-radius: inherit;
16
+ opacity: var(--mk-chat-background-opacity);
17
+ pointer-events: none;
18
+ }
19
+
20
+ .mk-chat-background::before,
21
+ .mk-chat-background::after,
22
+ mk-chat-background::before,
23
+ mk-chat-background::after {
24
+ position: absolute;
25
+ top: 50%;
26
+ left: 50%;
27
+ width: 800px;
28
+ height: 800px;
29
+ content: '';
30
+ opacity: var(--mk-chat-blob-opacity);
31
+ translate: -50% -50%;
32
+ scale: var(--mk-chat-scale);
33
+ filter: saturate(150%);
34
+ transition: opacity 1s ease;
35
+ }
36
+
37
+ .mk-chat-background::before,
38
+ mk-chat-background::before {
39
+ background: radial-gradient(circle at center, var(--mk-chat-blob-1) 0%, transparent 70%);
40
+ animation: mk-chat-blob-1 var(--mk-chat-orbit-1) linear infinite;
41
+ }
42
+
43
+ .mk-chat-background::after,
44
+ mk-chat-background::after {
45
+ background: radial-gradient(circle at center, var(--mk-chat-blob-2) 0%, transparent 70%);
46
+ animation: mk-chat-blob-2 var(--mk-chat-orbit-2) linear infinite;
47
+ }
48
+
49
+ /* Grain on top (Stepbro ships a PNG; this is SVG turbulence at the same 30 %). */
50
+ .mk-chat-background__noise {
51
+ position: absolute;
52
+ inset: 0;
53
+ z-index: 2;
54
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='220' height='220'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.9' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix values='0 0 0 0 .5 0 0 0 0 .5 0 0 0 0 .5 0 0 0 .55 0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
55
+ opacity: 0.3;
56
+ mix-blend-mode: overlay;
57
+ }
58
+
59
+ @keyframes mk-chat-blob-1 {
60
+ from {
61
+ transform: rotate(0) translate(150px) rotate(0);
62
+ }
63
+
64
+ to {
65
+ transform: rotate(360deg) translate(150px) rotate(-360deg);
66
+ }
67
+ }
68
+
69
+ @keyframes mk-chat-blob-2 {
70
+ from {
71
+ transform: rotate(180deg) translateY(120px) rotate(-180deg);
72
+ }
73
+
74
+ to {
75
+ transform: rotate(-180deg) translateY(120px) rotate(180deg);
76
+ }
77
+ }
78
+
79
+ @media (prefers-reduced-motion: reduce) {
80
+ .mk-chat-background::before,
81
+ .mk-chat-background::after,
82
+ mk-chat-background::before,
83
+ mk-chat-background::after {
84
+ animation: none;
85
+ }
86
+ }