// ==UserScript== // @name Confluence Plus // @namespace https://docs.scriptcat.org/ // @version 2.0.1 // @description Enhance your Confluence experience with extra features. // @author Shu Su // @homepage https://scriptcat.org/en/script-show-page/6271 // @homepageURL https://scriptcat.org/en/script-show-page/6271 // @match https://*.atlassian.net/wiki/* // @icon data:image/x-icon;base64,AAABAAEAQEAAAAEAIAAoQAAAFgAAACgAAABAAAAAgAAAAAEAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAANdqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAANdqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADXahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADXahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADXahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADXahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAANdqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/////////////////////////////////12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/wAAAADXahn/AAAAAAAAAAAAAAAAAAAAAAAAAADXahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ////////////////////////////////////////////12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/wAAAAAAAAAAAAAAAAAAAAAAAAAA12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf//////////////////////////////////////////////////////12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf8AAAAAAAAAAAAAAAAAAAAA12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ///////////////////////////////////////////////////////Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/////////////////////////////////12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/AAAAAAAAAAAAAAAAAAAAANdqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf///////////////////////////////////////////////////////////9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/////////////////////////////////////////////////Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/wAAAAAAAAAAAAAAAAAAAADXahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn//////////////////////////////////////////////////////////////////////9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ////////////////////////////////////////////////////////////12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf8AAAAAAAAAAAAAAADXahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/////////////////////////////////////////////////////////////////////////////////9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/////////////////////////////////////////////////////////////////9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/AAAAAAAAAADXahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/////////////////////////////////////////////////////////////////////////////////12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn////////////////////////////////////////////////////////////////////////////Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ///////////////////////////////////////////////////////////////////////////////////////Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf//////////////////////////////////////////////////////////////////////////////////////12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn////////////////////////////////////////////////////////////////////////////////////////////////////////////Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ////////////////////////////////////////////////////////////////////////////12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ////////////////////////////////////////////////////////////////////////////////////////////12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf//////12oZ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/////////////////////////////////////////////////////////////////////////////////////////////////////////////////12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf//////////////////////////////////////////////////////12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/////////////////////////////////////////////////////////////////////////////////9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/////////////////////////////////////////////////9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/////////////////////////////////////////////////////////////////////////////////9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/////////////////////////////////////////////////12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf//////////////////////////////////////////////////////////////////////12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/////////////////////////////////////////////////9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf8AAAAAAAAAANdqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn////////////Xahn////////////////////////////////////////////Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/////////////////////////////////Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf8AAAAAAAAAAAAAAADXahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ///////Xahn/12oZ/9dqGf///////////////////////////9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/////////////////12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/AAAAAAAAAAAAAAAA12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ////////////12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/AAAAAAAAAAAAAAAAAAAAAAAAAADXahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/wAAAAAAAAAAAAAAAAAAAAAAAAAA12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADXahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADXahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAANdqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAANdqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADXahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADXahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/9dqGf/Xahn/12oZ/wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA // @grant GM_getValue // @grant GM_setValue // @grant GM_xmlhttpRequest // @grant GM_addStyle // @grant GM_info // @connect * // @run-at document-idle // @license MIT // ==/UserScript== /* global GM_info */ /* global GM_xmlhttpRequest */ /* global GM_getValue, GM_setValue */ /* global GM_addStyle */ (() => { // src/plugins/confluence/runtime/utils.js var Utils = { extractPageId(url = location.href) { const source = String(url || ""); const match = source.match(/\/pages\/(\d+)/i) || source.match(/\/page\/(\d+)/i) || source.match(/[?&#]pageId=(\d+)/i) || source.match(/[?&#]homepageId=(\d+)/i); if (match) { return match[1]; } try { const queryPageId = new URL(source, location.href).searchParams.get( "homepageId" ); if (/^\d+$/.test(queryPageId || "")) { return queryPageId; } } catch { } const meta = document.querySelector( 'meta[name="ajs-page-id"], meta[name="ajs-content-id"]' ); const dataElement = document.querySelector( "[data-page-id], [data-content-id]" ); const fallback = meta?.content || dataElement?.getAttribute("data-page-id") || dataElement?.getAttribute("data-content-id") || ""; return /^\d+$/.test(fallback) ? fallback : ""; }, isValidPageId(pageId) { return /^\d+$/.test(pageId); }, isBlogPostUrl(url = location.href) { try { const pathname = new URL(url, location.href).pathname; return /\/spaces\/[^/]+\/blog(?:\/|$)/i.test(pathname); } catch { return /\/spaces\/[^/]+\/blog(?:\/|$)/i.test(String(url || "")); } }, debounce(fn, delay = 300) { let timer; return (...args) => { clearTimeout(timer); timer = setTimeout(() => fn(...args), delay); }; } }; // src/plugins/confluence/runtime/config.js var Config = { storageKey: "confluence_plus_state", api: { v1Path: "/wiki/rest/api", v2Path: "/wiki/api/v2" }, request: { timeout: 15e3 }, ai: { defaultApiVersion: "2025-03-01-preview", defaultModelsApiVersion: "2024-10-21" }, ui: { defaultWidth: 420, defaultHeight: 700, minWidth: 320, minHeight: 400 }, settings: { defaultLanguage: "system", defaultTheme: "system", defaultFloatingButtonRight: 120, minFloatingButtonRight: 0, maxFloatingButtonRight: 1e3 } }; // src/plugins/confluence/runtime/logger.js var Logger = { info(event, data) { if (typeof data === "undefined") { console.info("[Confluence Plus] " + event); return; } console.info("[Confluence Plus] " + event, data); }, warn(event, data) { console.warn("[Confluence Plus] " + event, data); }, error(event, error) { console.error("[Confluence Plus] " + event, error); } }; // src/plugins/confluence/runtime/storage.js var Storage = { loadData() { try { const raw = GM_getValue(Config.storageKey, "{}"); const data = JSON.parse(raw); if (data.auth) { delete data.auth; GM_setValue(Config.storageKey, JSON.stringify(data)); } return data; } catch (error) { Logger.error("storage.load-failed", error); return {}; } }, loadUi() { const ui = this.loadData().ui || {}; const result = {}; if (typeof ui.width === "number") result.width = ui.width; if (typeof ui.height === "number") result.height = ui.height; return result; }, saveUi(ui) { const data = this.loadData(); data.ui = { width: ui.width, height: ui.height }; GM_setValue(Config.storageKey, JSON.stringify(data)); }, loadAi() { return this.loadData().ai || {}; }, saveAi(ai) { const data = this.loadData(); data.ai = ai; GM_setValue(Config.storageKey, JSON.stringify(data)); }, clearAi() { const data = this.loadData(); delete data.ai; GM_setValue(Config.storageKey, JSON.stringify(data)); }, loadSettings() { return this.loadData().settings || {}; }, saveSettings(settings) { const data = this.loadData(); data.settings = settings; GM_setValue(Config.storageKey, JSON.stringify(data)); } }; // src/plugins/confluence/runtime/router.js function createRouter({ Store: Store2, Logger: Logger2, Utils: Utils2 }) { let lastUrl = ""; function getContext() { const url = location.href; return { url, pageId: Utils2.extractPageId(url) }; } function emitIfChanged() { const context = getContext(); if (context.url === lastUrl) return; lastUrl = context.url; Store2.resetPanelSession(); Store2.setContext(context); Logger2.info("route.changed", context); } function init() { const rawPushState = history.pushState; const rawReplaceState = history.replaceState; history.pushState = function(...args) { rawPushState.apply(this, args); setTimeout(emitIfChanged, 50); }; history.replaceState = function(...args) { rawReplaceState.apply(this, args); setTimeout(emitIfChanged, 50); }; window.addEventListener("popstate", emitIfChanged); emitIfChanged(); } return { init, getContext }; } // src/plugins/confluence/runtime/http.js function createHttp({ Config: Config2, getLocaleText: getLocaleText2 }) { const Http2 = { requestViaManager(url, options = {}) { const headers = { Accept: "application/json", ...options.headers || {} }; const logRequest = options.logLabel ? { method: options.method || "GET", url, headers: Object.fromEntries( Object.entries(headers).map(([key, value]) => [ key, /api-key/i.test(key) ? "[REDACTED]" : value ]) ), body: options.body } : null; if (logRequest) { console.info(`[Confluence Plus] ${options.logLabel} request`, logRequest); } return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: options.method || "GET", url, data: options.body, headers, timeout: Config2.request.timeout, onload(response) { if (logRequest) { console.info(`[Confluence Plus] ${options.logLabel} response`, { status: response.status, responseText: response.responseText }); } if (response.status < 200 || response.status >= 300) { const detail = (response.responseText || "").replace(/\s+/g, " ").slice(0, 500); reject(new Error( `HTTP ${response.status}: ${detail}` )); return; } if (response.status === 204) { resolve(null); return; } try { resolve(JSON.parse(response.responseText)); } catch { reject(new Error( getLocaleText2("azureNonJsonResponse") )); } }, onerror() { reject(new Error(getLocaleText2("azureCrossOriginRequestFailed"))); }, ontimeout() { reject(new Error(getLocaleText2("azureApiTimedOut"))); } }); }); }, async request(url, options = {}) { if (options.crossOrigin) { if (typeof GM_xmlhttpRequest !== "function") { throw new Error(getLocaleText2("gmRequestUnavailable")); } return this.requestViaManager(url, options); } const controller = new AbortController(); const timeoutId = setTimeout(() => { controller.abort(); }, Config2.request.timeout); try { const response = await fetch(url, { ...options, credentials: options.credentials || "same-origin", signal: controller.signal, headers: { Accept: "application/json", ...options.headers || {} } }); if (!response.ok) { const text = await response.text(); const detail = text.replace(/\s+/g, " ").slice(0, 500); throw new Error( `HTTP ${response.status}: ${detail}` ); } if (response.status === 204) { return null; } const contentType = response.headers.get("content-type") || ""; if (!contentType.includes("application/json")) { throw new Error( getLocaleText2("confluenceNonJsonResponse") ); } return await response.json(); } catch (error) { if (error.name === "AbortError") { throw new Error(getLocaleText2("confluenceApiTimedOut")); } throw error; } finally { clearTimeout(timeoutId); } }, get(url, options = {}) { return this.request(url, { method: "GET", ...options }); }, post(url, body, options = {}) { return this.request(url, { ...options, method: "POST", body: JSON.stringify(body), headers: { "Content-Type": "application/json", ...options.headers || {} } }); } }; return Http2; } // src/plugins/confluence/runtime/locale-messages.js var localeMessages = { en: { settings: "Confluence Plus settings", configure: "Configure Confluence Plus", close: "Close", general: "General", contentModule: "Content", content: "Content", language: "Language", languageDescription: "Choose the plugin display language.", theme: "Theme", themeDescription: "Light, dark, or follow system.", floatingPosition: "Floating button position", floatingPositionDescription: "Distance from the right edge of the screen.", system: "System", english: "English", chinese: "\u4E2D\u6587", light: "Light", dark: "Dark", pixels: "px", noSettings: "No settings available yet.", aiConfiguration: "AI Configuration", aiConfigurationDescription: "Configure Azure OpenAI for Confluence content features.", aiStatusNotConfigured: "Not configured", aiStatusConfigured: "Configured", aiStatusAvailable: "Configured", aiStatusUnavailable: "Unavailable", apiEndpoint: "API endpoint", apiEndpointDescription: "For example: https://your-resource.openai.azure.com/", apiKey: "API Key", apiVersion: "API version", apiVersionDescription: "Azure OpenAI API version used for chat completions.", apiKeyPlaceholder: "Enter Azure OpenAI API Key", show: "Show", hide: "Hide", modelDeployment: "Deployment", modelDeploymentDescription: "Enter the deployment name used by Azure OpenAI.", modelDeploymentPlaceholder: "Enter a deployment name", tokenParameter: "Token parameter", tokenParameterDescription: "Newer reasoning models use max_completion_tokens; older models use max_tokens.", maxCompletionTokens: "max_completion_tokens", maxTokens: "max_tokens", notTested: "Connection has not been tested.", connecting: "Connecting to Azure OpenAI using the specified deployment\u2026", testConnection: "Test connection", testConnectionTooltip: "Test the connection using the specified deployment.", clear: "Clear", missingEndpointOrKey: "Endpoint and API Key are required.", deploymentRequired: "Enter a deployment name first.", deploymentVerificationFailed: "Deployment verification returned an invalid response.", deploymentVerified: "Deployment connection succeeded.", contentToolTitle: "Content Management Tool", contentToolSubtitle: "Use Tool to manage the content", spacePageTree: "Space Page Tree", spacePageTreeDescription: "Browse and expand the complete page tree of the current Space.", generateLabel: "Generate Label", generateLabelDescription: "Analyze the current page and suggest labels using the configured AI.", aiConfigurationRequired: "AI configuration required:", currentSpaceIcon: "Current Space icon", leftClick: "Left-click: Open feature window", rightClick: "Right-click: Open configuration window", maximizeWindow: "Maximize window", restoreWindow: "Restore window", closeWindow: "Close window", openPage: "Open page", loadingSpaceTree: "Loading Space page tree\u2026", reloadSpaceTree: "Reload the Space page tree", collapseChildren: "Collapse children", expandChildren: "Expand children", childPages: "{{count}} child page(s)", currentPageBadge: "Current", noPagesInSpace: "No pages found in this Space.", loadedPages: "Loaded {{count}} page(s)", loadedPagesFromCache: "Loaded {{count}} page(s) from session cache", generateSuggestionsForPage: "Generate suggestions for the current page", pageContentSentToAi: "The page title and content will be sent to the configured Azure OpenAI endpoint.", generateLabelSuggestions: "Analyze the current page and generate label suggestions", generateSuggestions: "Generate Suggestions", recommendedLabelsFor: "Recommended Labels for {{title}}", recommendedLabels: "Recommended Labels", addLabelManually: "Add a label manually", add: "Add", applySelectedLabels: "Apply Selected Labels", applySelectedLabelsTitle: "Add the selected labels to the current Confluence page", removeLabel: "Remove label", applyLabelsQuestion: "Apply labels?", applyOneLabelQuestion: "Apply 1 selected label?", applyManyLabelsQuestion: "Apply the {{count}} selected labels?", applyLabelsMessage: "Add to the current page: {{labels}}", cancel: "Cancel", apply: "Apply", selectNewValidLabel: "Select at least one new valid label", noLabelsToApplyTooltip: "Select at least one new valid label to apply", applyingLabels: "Applying labels\u2026", addedLabelsRefreshing: "Added {{count}} label(s). Refreshing page\u2026", loadingAndGenerating: "Loading page content and generating suggestions\u2026", configureAiBeforeLabels: "Configure AI before generating labels", generatedSuggestions: "Generated {{count}} suggestion(s)", noNewLabelSuggestions: "No new label suggestions were generated", labelsCount: "{{count}} label(s): {{labels}}", aiConfigurationRequiredError: "Configure and save a valid AI configuration first.", aiLabelResponseMissing: "AI did not return recognizable Label suggestions.", aiLabelResponseInvalid: "AI returned invalid JSON for the Label suggestions.", invalidPageId: "The current page does not have a valid Confluence Page ID.", missingSpaceId: "The current page does not have a recognizable Space ID.", missingSpaceInfo: "The current Space does not have a recognizable name or Space Key.", endpointHttpsRequired: "Azure OpenAI endpoint must use HTTPS.", unsupportedV1Endpoint: "Azure OpenAI /openai/v1 endpoint is not supported yet; use an Azure resource endpoint.", endpointMustNotContainCredentials: "Endpoint must not contain credentials, query parameters, or a hash.", endpointRequired: "Enter an Azure OpenAI endpoint.", apiKeyRequired: "Enter an Azure OpenAI API key.", apiVersionRequired: "API version cannot be empty.", azureNonJsonResponse: "Azure OpenAI returned a non-JSON response.", azureCrossOriginRequestFailed: "Azure OpenAI cross-origin request failed.", azureApiTimedOut: "Azure OpenAI API request timed out.", gmRequestUnavailable: "The UserScript cross-origin request API is unavailable. Grant the script GM_xmlhttpRequest permission.", confluenceNonJsonResponse: "Confluence returned a non-JSON response; you may need to sign in again.", confluenceApiTimedOut: "Confluence API request timed out." }, "zh-CN": { settings: "Confluence Plus \u8BBE\u7F6E", configure: "\u914D\u7F6E Confluence Plus", close: "\u5173\u95ED", general: "\u5E38\u89C4", contentModule: "\u5185\u5BB9", content: "\u5185\u5BB9", language: "\u8BED\u8A00", languageDescription: "\u9009\u62E9\u63D2\u4EF6\u663E\u793A\u8BED\u8A00\u3002", theme: "\u4E3B\u9898", themeDescription: "\u6D45\u8272\u3001\u6DF1\u8272\u6216\u8DDF\u968F\u7CFB\u7EDF\u3002", floatingPosition: "\u6D6E\u52A8\u6309\u94AE\u4F4D\u7F6E", floatingPositionDescription: "\u8DDD\u79BB\u5C4F\u5E55\u53F3\u4FA7\u7684\u8DDD\u79BB\u3002", system: "\u7CFB\u7EDF", english: "English", chinese: "\u4E2D\u6587", light: "\u6D45\u8272", dark: "\u6DF1\u8272", pixels: "px", noSettings: "\u6682\u65F6\u6CA1\u6709\u53EF\u914D\u7F6E\u9879\u3002", aiConfiguration: "AI \u914D\u7F6E", aiConfigurationDescription: "\u914D\u7F6E Azure OpenAI\uFF0C\u4EE5\u4F9B Confluence \u5185\u5BB9\u529F\u80FD\u8C03\u7528\u3002", aiStatusNotConfigured: "\u672A\u914D\u7F6E", aiStatusConfigured: "\u5DF2\u914D\u7F6E", aiStatusAvailable: "\u5DF2\u914D\u7F6E", aiStatusUnavailable: "\u4E0D\u53EF\u7528", apiEndpoint: "API \u7AEF\u70B9", apiEndpointDescription: "\u4F8B\u5982\uFF1Ahttps://your-resource.openai.azure.com/", apiKey: "API Key", apiVersion: "API \u7248\u672C", apiVersionDescription: "\u7528\u4E8E Chat Completions \u7684 Azure OpenAI API \u7248\u672C\u3002", apiKeyPlaceholder: "\u8F93\u5165 Azure OpenAI API Key", show: "\u663E\u793A", hide: "\u9690\u85CF", modelDeployment: "\u90E8\u7F72", modelDeploymentDescription: "\u8F93\u5165 Azure OpenAI \u4F7F\u7528\u7684\u90E8\u7F72\u540D\u79F0\u3002", modelDeploymentPlaceholder: "\u8BF7\u8F93\u5165\u90E8\u7F72\u540D\u79F0", tokenParameter: "Token \u6570\u91CF\u53C2\u6570", tokenParameterDescription: "\u8F83\u65B0\u7684\u63A8\u7406\u6A21\u578B\u4F7F\u7528 max_completion_tokens\uFF0C\u65E7\u6A21\u578B\u4F7F\u7528 max_tokens\u3002", maxCompletionTokens: "max_completion_tokens", maxTokens: "max_tokens", notTested: "\u5C1A\u672A\u6D4B\u8BD5\u8FDE\u63A5\u3002", connecting: "\u6B63\u5728\u4F7F\u7528\u6307\u5B9A\u7684\u90E8\u7F72\u8FDE\u63A5 Azure OpenAI\u2026", testConnection: "\u6D4B\u8BD5\u8FDE\u63A5", testConnectionTooltip: "\u4F7F\u7528\u6307\u5B9A\u7684\u90E8\u7F72\u6D4B\u8BD5\u8FDE\u63A5\u3002", clear: "\u6E05\u9664", missingEndpointOrKey: "Endpoint \u548C API Key \u4E0D\u80FD\u4E3A\u7A7A\u3002", deploymentRequired: "\u8BF7\u5148\u8F93\u5165\u90E8\u7F72\u540D\u79F0\u3002", deploymentVerificationFailed: "\u90E8\u7F72\u9A8C\u8BC1\u8FD4\u56DE\u4E86\u65E0\u6548\u54CD\u5E94\u3002", deploymentVerified: "\u90E8\u7F72\u8FDE\u63A5\u6210\u529F\u3002", contentToolTitle: "\u5185\u5BB9\u7BA1\u7406\u5DE5\u5177", contentToolSubtitle: "\u4F7F\u7528\u5DE5\u5177\u7BA1\u7406\u5185\u5BB9", spacePageTree: "Space \u9875\u9762\u6811", spacePageTreeDescription: "\u6D4F\u89C8\u5E76\u5C55\u5F00\u5F53\u524D Space \u7684\u5B8C\u6574\u9875\u9762\u6811\u3002", generateLabel: "\u751F\u6210 Label", generateLabelDescription: "\u4F7F\u7528\u5DF2\u914D\u7F6E\u7684 AI \u5206\u6790\u5F53\u524D\u9875\u9762\u5E76\u751F\u6210\u6807\u7B7E\u5EFA\u8BAE\u3002", aiConfigurationRequired: "\u9700\u8981 AI \u914D\u7F6E\uFF1A", currentSpaceIcon: "\u5F53\u524D Space \u56FE\u6807", leftClick: "\u5DE6\u952E\uFF1A\u6253\u5F00\u529F\u80FD\u7A97\u53E3", rightClick: "\u53F3\u952E\uFF1A\u6253\u5F00\u914D\u7F6E\u7A97\u53E3", maximizeWindow: "\u6700\u5927\u5316\u7A97\u53E3", restoreWindow: "\u8FD8\u539F\u7A97\u53E3", closeWindow: "\u5173\u95ED\u7A97\u53E3", openPage: "\u6253\u5F00\u9875\u9762", loadingSpaceTree: "\u6B63\u5728\u52A0\u8F7D Space \u9875\u9762\u6811\u2026", reloadSpaceTree: "\u91CD\u65B0\u52A0\u8F7D Space \u9875\u9762\u6811", collapseChildren: "\u6536\u8D77\u5B50\u9875\u9762", expandChildren: "\u5C55\u5F00\u5B50\u9875\u9762", childPages: "{{count}} \u4E2A\u5B50\u9875\u9762", currentPageBadge: "\u5F53\u524D\u9875\u9762", noPagesInSpace: "\u5F53\u524D Space \u4E2D\u6CA1\u6709\u627E\u5230\u9875\u9762\u3002", loadedPages: "\u5DF2\u52A0\u8F7D {{count}} \u4E2A\u9875\u9762", loadedPagesFromCache: "\u5DF2\u4ECE\u4F1A\u8BDD\u7F13\u5B58\u52A0\u8F7D {{count}} \u4E2A\u9875\u9762", generateSuggestionsForPage: "\u4E3A\u5F53\u524D\u9875\u9762\u751F\u6210\u5EFA\u8BAE", pageContentSentToAi: "\u9875\u9762\u6807\u9898\u548C\u5185\u5BB9\u5C06\u53D1\u9001\u5230\u5DF2\u914D\u7F6E\u7684 Azure OpenAI \u7AEF\u70B9\u3002", generateLabelSuggestions: "\u5206\u6790\u5F53\u524D\u9875\u9762\u5E76\u751F\u6210\u6807\u7B7E\u5EFA\u8BAE", generateSuggestions: "\u751F\u6210\u5EFA\u8BAE", recommendedLabelsFor: "{{title}} \u7684\u63A8\u8350\u6807\u7B7E", recommendedLabels: "\u63A8\u8350\u6807\u7B7E", addLabelManually: "\u624B\u52A8\u6DFB\u52A0\u6807\u7B7E", add: "\u6DFB\u52A0", applySelectedLabels: "\u5E94\u7528\u9009\u4E2D\u7684\u6807\u7B7E", applySelectedLabelsTitle: "\u5C06\u9009\u4E2D\u7684\u6807\u7B7E\u6DFB\u52A0\u5230\u5F53\u524D Confluence \u9875\u9762", removeLabel: "\u79FB\u9664\u6807\u7B7E", applyLabelsQuestion: "\u5E94\u7528\u6807\u7B7E\uFF1F", applyOneLabelQuestion: "\u5E94\u7528\u9009\u4E2D\u7684 1 \u4E2A\u6807\u7B7E\uFF1F", applyManyLabelsQuestion: "\u5E94\u7528\u9009\u4E2D\u7684 {{count}} \u4E2A\u6807\u7B7E\uFF1F", applyLabelsMessage: "\u5C06\u4EE5\u4E0B\u6807\u7B7E\u6DFB\u52A0\u5230\u5F53\u524D\u9875\u9762\uFF1A{{labels}}", cancel: "\u53D6\u6D88", apply: "\u5E94\u7528", selectNewValidLabel: "\u81F3\u5C11\u9009\u62E9\u4E00\u4E2A\u6709\u6548\u7684\u65B0\u6807\u7B7E", noLabelsToApplyTooltip: "\u8BF7\u81F3\u5C11\u9009\u62E9\u4E00\u4E2A\u6709\u6548\u7684\u65B0\u6807\u7B7E", applyingLabels: "\u6B63\u5728\u5E94\u7528\u6807\u7B7E\u2026", addedLabelsRefreshing: "\u5DF2\u6DFB\u52A0 {{count}} \u4E2A\u6807\u7B7E\uFF0C\u6B63\u5728\u5237\u65B0\u9875\u9762\u2026", loadingAndGenerating: "\u6B63\u5728\u52A0\u8F7D\u9875\u9762\u5185\u5BB9\u5E76\u751F\u6210\u5EFA\u8BAE\u2026", configureAiBeforeLabels: "\u751F\u6210\u6807\u7B7E\u524D\u8BF7\u5148\u914D\u7F6E AI", generatedSuggestions: "\u5DF2\u751F\u6210 {{count}} \u6761\u5EFA\u8BAE", noNewLabelSuggestions: "\u672A\u751F\u6210\u65B0\u7684\u6807\u7B7E\u5EFA\u8BAE", labelsCount: "{{count}} \u4E2A Label\uFF1A{{labels}}", aiConfigurationRequiredError: "\u8BF7\u5148\u914D\u7F6E\u5E76\u4FDD\u5B58\u53EF\u7528\u7684 AI \u914D\u7F6E\u3002", aiLabelResponseMissing: "AI \u672A\u8FD4\u56DE\u53EF\u8BC6\u522B\u7684 Label \u5EFA\u8BAE\u3002", aiLabelResponseInvalid: "AI \u8FD4\u56DE\u7684 Label \u5EFA\u8BAE\u4E0D\u662F\u6709\u6548 JSON\u3002", invalidPageId: "\u5F53\u524D\u9875\u9762\u6CA1\u6709\u6709\u6548\u7684 Confluence Page ID\u3002", missingSpaceId: "\u5F53\u524D\u9875\u9762\u6CA1\u6709\u53EF\u8BC6\u522B\u7684 Space ID\u3002", missingSpaceInfo: "\u5F53\u524D Space \u6CA1\u6709\u53EF\u8BC6\u522B\u7684\u540D\u79F0\u6216 Space Key\u3002", endpointHttpsRequired: "Azure OpenAI \u7AEF\u70B9\u5FC5\u987B\u4F7F\u7528 HTTPS\u3002", unsupportedV1Endpoint: "\u6682\u4E0D\u652F\u6301 Azure OpenAI /openai/v1 \u7AEF\u70B9\uFF0C\u8BF7\u4F7F\u7528 Azure resource endpoint\u3002", endpointMustNotContainCredentials: "Endpoint \u4E0D\u5E94\u5305\u542B\u8D26\u53F7\u3001\u67E5\u8BE2\u53C2\u6570\u6216 hash\u3002", endpointRequired: "\u8BF7\u8F93\u5165 Azure OpenAI Endpoint\u3002", apiKeyRequired: "\u8BF7\u8F93\u5165 Azure OpenAI API Key\u3002", apiVersionRequired: "API Version \u4E0D\u80FD\u4E3A\u7A7A\u3002", azureNonJsonResponse: "Azure OpenAI \u8FD4\u56DE\u4E86\u975E JSON \u54CD\u5E94\u3002", azureCrossOriginRequestFailed: "Azure OpenAI \u8DE8\u57DF\u8BF7\u6C42\u5931\u8D25\u3002", azureApiTimedOut: "Azure OpenAI API \u8BF7\u6C42\u8D85\u65F6\u3002", gmRequestUnavailable: "UserScript \u8DE8\u57DF\u8BF7\u6C42\u63A5\u53E3\u4E0D\u53EF\u7528\uFF0C\u8BF7\u786E\u8BA4\u811A\u672C\u5DF2\u6388\u4E88 GM_xmlhttpRequest \u6743\u9650\u3002", confluenceNonJsonResponse: "Confluence \u8FD4\u56DE\u4E86\u975E JSON \u54CD\u5E94\uFF0C\u53EF\u80FD\u9700\u8981\u91CD\u65B0\u767B\u5F55\u3002", confluenceApiTimedOut: "Confluence API \u8BF7\u6C42\u8D85\u65F6\u3002" } }; // src/shared/locale.js var VALID_UI_LANGUAGES = ["system", "en", "zh-CN"]; function resolveUiLanguage(value, browserLanguage = navigator.language || "") { return value === "zh-CN" || value === "system" && /^zh(?:-|$)/i.test(browserLanguage) ? "zh-CN" : "en"; } function interpolateLocaleText(text, key, variables = {}) { return (text || key).replace(/\{\{(\w+)\}\}/g, (_, name) => variables[name] ?? ""); } // src/shared/runtime/locale-runtime.js function lookupMessage(messages, language, key) { const languageMessages = messages?.[language]; if (languageMessages && Object.prototype.hasOwnProperty.call(languageMessages, key)) { return languageMessages[key]; } const parts = key.split("."); let entry = messages; parts.forEach((part) => { entry = entry?.[part]; }); return entry?.[language]; } function createLocaleRuntime({ messages, getLanguage, browserLanguage }) { const resolveLanguage = (value = getLanguage()) => resolveUiLanguage(value, browserLanguage?.() ?? navigator.language ?? ""); const getLocaleText2 = (key, value = getLanguage(), variables = {}) => interpolateLocaleText(lookupMessage(messages, resolveLanguage(value), key), key, variables); return { validUiLanguages: VALID_UI_LANGUAGES, resolveUiLanguage: resolveLanguage, getLocaleText: getLocaleText2 }; } // src/plugins/confluence/runtime/locale.js function createLocaleService({ getLanguage, localeMessages: localeMessages2 }) { const { validUiLanguages, resolveUiLanguage: resolveUiLanguage2, getLocaleText: getLocaleText2 } = createLocaleRuntime({ messages: localeMessages2, getLanguage }); return { validUiLanguages, resolveUiLanguage: resolveUiLanguage2, getLocaleText: getLocaleText2 }; } // src/plugins/confluence/runtime/confluence-api.js function createConfluenceApi({ Config: Config2, Http: Http2, origin = location.origin }) { const api = { getV1Url(path) { return origin + Config2.api.v1Path + path; }, getV2Url(path) { return origin + Config2.api.v2Path + path; }, folders: { get(folderId) { return Http2.get(api.getV2Url("/folders/" + encodeURIComponent(folderId))); } }, spaces: { get(spaceId) { return Http2.get(api.getV2Url("/spaces/" + encodeURIComponent(spaceId))); }, async getByKey(spaceKey) { const query = new URLSearchParams({ keys: spaceKey, limit: "1" }); const data = await Http2.get(api.getV2Url("/spaces") + "?" + query); return Array.isArray(data.results) ? data.results[0] : null; } }, pages: { get(pageId) { return Http2.get(api.getV2Url("/pages/" + pageId)); }, getForAi(pageId) { const query = new URLSearchParams({ expand: "body.storage,metadata.labels" }); return Http2.get(api.getV1Url("/content/" + pageId) + "?" + query); }, addLabels(pageId, labels) { return Http2.post(api.getV1Url("/content/" + pageId + "/label"), labels.map((name) => ({ prefix: "global", name }))); }, getChildren(pageId) { return Http2.get(api.getV2Url("/pages/" + pageId + "/children")); }, getSpacePages(spaceId, cursor = "") { const query = new URLSearchParams({ limit: "250" }); if (cursor) query.set("cursor", cursor); return Http2.get(api.getV2Url("/spaces/" + encodeURIComponent(spaceId) + "/pages") + "?" + query); } }, pageStates: { get(pageId) { return Http2.get(api.getV1Url("/content/" + pageId + "/state")); } } }; return api; } // src/plugins/confluence/runtime/ai-config.js function createAIConfig({ Config: Config2, Storage: Storage2, getLocaleText: getLocaleText2 }) { const AIConfig2 = (() => { const defaults = { schemaVersion: 2, endpoint: "", apiKey: "", apiVersion: Config2.ai.defaultApiVersion, modelsApiVersion: Config2.ai.defaultModelsApiVersion, deployment: "", deployments: [], tokenParameter: "max_completion_tokens", lastCheck: null }; const stored = Storage2.loadAi(); let config = stored?.schemaVersion === defaults.schemaVersion ? { ...defaults, ...stored, deployments: Array.isArray(stored.deployments) ? [...stored.deployments] : [] } : JSON.parse(JSON.stringify(defaults)); if (stored?.schemaVersion !== defaults.schemaVersion) { Storage2.saveAi(config); } function normalizeEndpoint(endpoint) { const value = endpoint.trim().replace(/\/+$/, ""); const url = new URL(value); if (url.protocol !== "https:") { throw new Error(getLocaleText2("endpointHttpsRequired")); } if (/\/openai\/v1\/?$/i.test(url.pathname)) { throw new Error(getLocaleText2("unsupportedV1Endpoint")); } if (url.username || url.password || url.search || url.hash) { throw new Error(getLocaleText2("endpointMustNotContainCredentials")); } return url.origin + url.pathname.replace(/\/+$/, ""); } function validate(candidate) { const next = { ...config, ...candidate }; if (!next.endpoint) { throw new Error(getLocaleText2("endpointRequired")); } if (!next.apiKey) { throw new Error(getLocaleText2("apiKeyRequired")); } next.endpoint = normalizeEndpoint(next.endpoint); next.apiVersion = next.apiVersion.trim(); next.modelsApiVersion = next.modelsApiVersion.trim(); next.deployment = next.deployment.trim(); next.tokenParameter = ["max_tokens", "max_completion_tokens"].includes(next.tokenParameter) ? next.tokenParameter : defaults.tokenParameter; if (!next.apiVersion || !next.modelsApiVersion) { throw new Error(getLocaleText2("apiVersionRequired")); } return next; } return { get() { return { ...config, deployments: [...config.deployments] }; }, validate, save(candidate) { const previous = config; const next = validate(candidate); const changed = previous.endpoint !== next.endpoint || previous.apiKey !== next.apiKey || previous.apiVersion !== next.apiVersion || previous.deployment !== next.deployment || previous.tokenParameter !== next.tokenParameter; const hasExplicitHealth = Object.prototype.hasOwnProperty.call( candidate, "lastCheck" ); config = { ...next, lastCheck: hasExplicitHealth ? next.lastCheck : changed ? null : next.lastCheck || null }; Storage2.saveAi(config); return this.get(); }, updateDraft(candidate = {}) { const next = { ...config, ...candidate, endpoint: String(candidate.endpoint ?? config.endpoint ?? "").trim(), apiKey: String(candidate.apiKey ?? config.apiKey ?? "").trim(), apiVersion: String(candidate.apiVersion ?? config.apiVersion ?? defaults.apiVersion).trim(), modelsApiVersion: String(candidate.modelsApiVersion ?? config.modelsApiVersion ?? defaults.modelsApiVersion).trim(), deployment: String(candidate.deployment ?? config.deployment ?? "").trim(), tokenParameter: ["max_tokens", "max_completion_tokens"].includes(candidate.tokenParameter) ? candidate.tokenParameter : config.tokenParameter || defaults.tokenParameter, deployments: Array.isArray(candidate.deployments) ? [...candidate.deployments] : [...config.deployments] }; if (next.endpoint) { try { next.endpoint = normalizeEndpoint(next.endpoint); } catch { next.endpoint = ""; } } const healthChanged = next.endpoint !== config.endpoint || next.apiKey !== config.apiKey || next.apiVersion !== config.apiVersion || next.deployment !== config.deployment || next.tokenParameter !== config.tokenParameter; config = { ...next, lastCheck: healthChanged ? null : next.lastCheck || null }; Storage2.saveAi(config); return this.get(); }, recordHealth(ok, message, metadata = {}) { config = { ...config, lastCheck: { ok, message, endpoint: config.endpoint, deployment: config.deployment, apiVersion: config.apiVersion, tokenParameter: config.tokenParameter, checkedAt: Date.now(), ...metadata } }; Storage2.saveAi(config); }, getStatus() { if (!config.endpoint || !config.apiKey || !config.deployment) { return { key: "not-configured", label: getLocaleText2("aiStatusNotConfigured") }; } if (!config.lastCheck || config.lastCheck.endpoint !== config.endpoint || config.lastCheck.deployment !== config.deployment || config.lastCheck.apiVersion !== config.apiVersion || config.lastCheck.tokenParameter !== config.tokenParameter) { return { key: "unverified", label: getLocaleText2("aiStatusConfigured") }; } return config.lastCheck.ok ? { key: "available", label: getLocaleText2("aiStatusConfigured") } : { key: "unavailable", label: getLocaleText2("aiStatusUnavailable") }; }, clear() { config = { ...defaults }; Storage2.clearAi(); }, isConfigured() { return Boolean(config.endpoint && config.apiKey); } }; })(); return AIConfig2; } // src/plugins/confluence/runtime/store.js function createStore({ Config: Config2, Storage: Storage2, Utils: Utils2 }) { const Store2 = (() => { const state = { context: { pageId: "", url: location.href }, ui: { x: 120, y: 120, width: Config2.ui.defaultWidth, height: Config2.ui.defaultHeight, minimized: true, maximized: false } }; const listeners = []; const persistUi = Utils2.debounce(() => { Storage2.saveUi(state.ui); }); function notify(type, value) { listeners.forEach((listener) => { listener(type, value, state); }); } return { init() { Object.assign(state.ui, Storage2.loadUi()); }, getContext() { return state.context; }, getUi() { return state.ui; }, setContext(context) { state.context = { ...state.context, ...context }; notify("context", state.context); }, updateUi(patch) { Object.assign(state.ui, patch); persistUi(); notify("ui", state.ui); }, resetPanelSession() { Object.assign(state.ui, { x: 120, y: 120, minimized: true, maximized: false }); notify("ui", state.ui); }, subscribe(listener) { listeners.push(listener); return () => { const index = listeners.indexOf(listener); if (index >= 0) { listeners.splice(index, 1); } }; } }; })(); return Store2; } // src/plugins/confluence/runtime/azure-openai.js function createAzureOpenAI({ Http: Http2, AIConfig: AIConfig2, getLocaleText: getLocaleText2 }) { const AzureOpenAI2 = { isV1Endpoint(endpoint) { return /\/openai\/v1$/i.test(endpoint); }, getUrl(endpoint, path, apiVersion) { if (this.isV1Endpoint(endpoint)) { return `${endpoint}${path}`; } const query = new URLSearchParams({ "api-version": apiVersion }); return `${endpoint}${path}?${query}`; }, getHeaders(apiKey) { return { "api-key": apiKey, Accept: "application/json" }; }, async verifyDeployment(config) { const response = await Http2.post( this.getChatCompletionsUrl(config), { messages: [ { role: "user", content: 'Return JSON exactly {"ok":true}.' } ], response_format: { type: "json_object" }, [config.tokenParameter]: 16 }, { crossOrigin: true, credentials: "omit", logLabel: "AI Test Connection", headers: this.getHeaders(config.apiKey) } ); if (!response || typeof response !== "object") { throw new Error(getLocaleText2("deploymentVerificationFailed")); } return response; }, async testConnection(candidate) { const config = AIConfig2.validate(candidate); if (!config.deployment) { throw new Error(getLocaleText2("deploymentRequired")); } await this.verifyDeployment(config); return { deployments: [{ id: config.deployment, label: config.deployment }], deploymentCount: 1, message: getLocaleText2("deploymentVerified") }; }, getChatCompletionsUrl(config) { const path = this.isV1Endpoint(config.endpoint) ? "/chat/completions" : `/openai/deployments/${encodeURIComponent(config.deployment)}/chat/completions`; return this.getUrl(config.endpoint, path, config.apiVersion); }, getResponsesUrl(config) { const path = this.isV1Endpoint(config.endpoint) ? "/responses" : `/openai/deployments/${encodeURIComponent(config.deployment)}/responses`; return this.getUrl(config.endpoint, path, config.apiVersion); }, extractChatOutputText(response) { return response?.choices?.[0]?.message?.content || ""; }, extractOutputText(response) { if (typeof response.output_text === "string") { return response.output_text; } const messages = response.output || []; const texts = []; messages.forEach((item) => { (item.content || []).forEach((content) => { if (content.type === "output_text" && content.text) { texts.push(content.text); } }); }); return texts.join("\n"); }, async suggestLabels({ title, content, existingLabels }) { const config = AIConfig2.get(); if (!AIConfig2.isConfigured() || !config.deployment) { throw new Error(getLocaleText2("aiConfigurationRequiredError")); } const prompt = [ "Recommend 3 to 10 concise Confluence labels for this page.", 'Return JSON only in the shape {"labels":["label-a"]}.', "Use lowercase letters, numbers, and hyphens only.", "Do not include existing labels or explanations.", `Existing labels: ${(existingLabels || []).join(", ") || "(none)"}`, `Title: ${title || "(untitled)"}`, "Page content:", content.slice(0, 24e3) ].join("\n\n"); const response = await Http2.post( this.getChatCompletionsUrl(config), { messages: [ { role: "user", content: prompt } ], response_format: { type: "json_object" }, [config.tokenParameter]: 6e3 }, { crossOrigin: true, credentials: "omit", headers: { ...this.getHeaders(config.apiKey) } } ); const output = this.extractChatOutputText(response); const match = output.match(/\{[\s\S]*\}/); if (!match) { throw new Error(getLocaleText2("aiLabelResponseMissing")); } let parsed; try { parsed = JSON.parse(match[0]); } catch { throw new Error(getLocaleText2("aiLabelResponseInvalid")); } return Array.isArray(parsed.labels) ? parsed.labels : []; } }; return AzureOpenAI2; } // src/plugins/confluence/runtime/ai-label-service.js function createAILabelService({ ConfluenceApi: ConfluenceApi2, AzureOpenAI: AzureOpenAI2 }) { const AILabelService2 = { normalize(label) { return String(label || "").trim().toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 80); }, getExistingLabels(page) { return (page.metadata?.labels?.results || []).map((item) => item.name).filter(Boolean); }, async generate(pageId) { const page = await ConfluenceApi2.pages.getForAi(pageId); const existingLabels = this.getExistingLabels(page); const body = page.body?.storage?.value || ""; const suggestions = await AzureOpenAI2.suggestLabels({ title: page.title, content: body, existingLabels }); const existing = new Set( existingLabels.map((label) => this.normalize(label)) ); const unique = []; suggestions.forEach((label) => { const normalized = this.normalize(label); if (normalized && !existing.has(normalized) && !unique.includes(normalized)) { unique.push(normalized); } }); return { page, existingLabels, suggestions: unique.slice(0, 10) }; } }; return AILabelService2; } // src/plugins/confluence/runtime/space-tree-service.js function createSpaceTreeService({ ConfluenceApi: ConfluenceApi2, Utils: Utils2, Logger: Logger2 }) { const SpaceTreeService2 = { getNextCursor(data) { const directCursor = data.meta?.cursor || data.cursor || ""; if (directCursor) { return directCursor; } const next = data._links?.next || data.links?.next; if (!next) { return ""; } try { return new URL(next, location.origin).searchParams.get("cursor") || ""; } catch { return ""; } }, loadFromSidebar(currentPageId) { const containers = [ ...document.querySelectorAll( '[role="tree"], nav, aside, [data-testid*="sidebar" i], [data-testid*="tree" i]' ) ]; let best = null; let bestScore = 0; containers.forEach((container) => { const links = [...container.querySelectorAll( 'a[href*="/pages/"]' )]; const pageLinks = links.filter((link) => { const id = Utils2.extractPageId(link.href); return id && link.getClientRects().length; }); const containsCurrent = pageLinks.some( (link) => Utils2.extractPageId(link.href) === String(currentPageId) ); const score = pageLinks.length + (containsCurrent ? 1e3 : 0); if (score > bestScore) { best = { container, links: pageLinks }; bestScore = score; } }); if (!best || !best.links.length) { return []; } const items = []; const seen = /* @__PURE__ */ new Set(); const stack = []; best.links.forEach((link) => { const id = Utils2.extractPageId(link.href); if (!id || seen.has(id)) return; seen.add(id); const treeItem = link.closest('[role="treeitem"]'); const ariaLevel = treeItem?.getAttribute("aria-level"); let depth = ariaLevel ? Number(ariaLevel) - 1 : NaN; if (!Number.isFinite(depth)) { depth = 0; let listItem = link.closest("li"); while (listItem && best.container.contains(listItem)) { depth += 1; listItem = listItem.parentElement?.closest("li"); } depth = Math.max(0, depth - 1); } depth = Math.max(0, depth); stack.length = Math.min(stack.length, depth); const title = (link.getAttribute("data-item-title") || link.getAttribute("aria-label") || link.textContent || "(Untitled)").trim(); const parent = depth > 0 ? stack[depth - 1] : null; const item = { id, title, parentId: parent ? parent.id : null, sidebarDepth: depth }; items.push(item); stack[depth] = item; stack.length = depth + 1; }); return items; }, async load(spaceId) { const pages = []; let cursor = ""; do { const data = await ConfluenceApi2.pages.getSpacePages( spaceId, cursor ); const results = Array.isArray(data.results) ? data.results : []; pages.push(...results); cursor = this.getNextCursor(data); } while (cursor); const folders = /* @__PURE__ */ new Map(); const pendingFolderIds = new Set( pages.filter((page) => page.parentType === "folder" && page.parentId).map((page) => String(page.parentId)) ); while (pendingFolderIds.size) { const folderId = pendingFolderIds.values().next().value; pendingFolderIds.delete(folderId); if (folders.has(folderId)) continue; try { const folder = await ConfluenceApi2.folders.get(folderId); if (!folder?.id) continue; const normalizedFolder = { ...folder, type: "folder", id: String(folder.id), parentId: folder.parentId || null, parentType: folder.parentType || "page" }; folders.set(normalizedFolder.id, normalizedFolder); if (normalizedFolder.parentType === "folder" && normalizedFolder.parentId) { pendingFolderIds.add(String(normalizedFolder.parentId)); } } catch (error) { Logger2.warn("space-tree.folder-load-failed", { folderId, error }); } } return [...pages, ...folders.values()]; }, buildTree(pages, currentPageId, spaceRootTitle) { const normalizedSpaceName = (spaceRootTitle || "").trim(); const spaceRoot = pages.find((page) => (page.title || "").trim() === normalizedSpaceName); const allowedIds = /* @__PURE__ */ new Set(); if (spaceRoot) { allowedIds.add(String(spaceRoot.id)); let changed = true; while (changed) { changed = false; pages.forEach((page) => { const parentId = page.parentId || page.parent?.id; if (parentId && allowedIds.has(String(parentId)) && !allowedIds.has(String(page.id))) { allowedIds.add(String(page.id)); changed = true; } }); } } const nodes = /* @__PURE__ */ new Map(); pages.forEach((page, index) => { const id = String(page.id); if (!allowedIds.has(id)) return; const parentId = page.id === spaceRoot?.id ? null : page.parentId || page.parent?.id || null; const rawPosition = page.position ?? page.positionInParent ?? page.order ?? page.ordinal ?? page.sortOrder; const position = Number(rawPosition); nodes.set(id, { id, title: page.title || "(Untitled)", parentId: parentId ? String(parentId) : null, children: [], page, position: Number.isFinite(position) ? position : null, sourceIndex: index }); }); const roots = []; nodes.forEach((node) => { const parent = node.parentId ? nodes.get(node.parentId) : null; if (parent && parent.id !== node.id) { parent.children.push(node); } else { roots.push(node); } }); const sortNodes = (items) => { items.sort((a, b) => { if (a.position !== null && b.position !== null) { return a.position - b.position || a.sourceIndex - b.sourceIndex; } if (a.position !== null) return -1; if (b.position !== null) return 1; return a.sourceIndex - b.sourceIndex; }); items.forEach((item) => sortNodes(item.children)); }; sortNodes(roots); const expanded = /* @__PURE__ */ new Set(); let current = nodes.get(String(currentPageId)); while (current) { expanded.add(current.id); current = current.parentId ? nodes.get(current.parentId) : null; } return { roots, nodes, expanded, filteredCount: nodes.size }; } }; return SpaceTreeService2; } // src/plugins/confluence/runtime/content-session-cache.js function createContentSessionCache({ Logger: Logger2 }) { const ContentSessionCache2 = (() => { const prefix = `confluence-plus:session:v1:${location.origin}:`; function read(name) { try { const value = sessionStorage.getItem(`${prefix}${name}`); return value ? JSON.parse(value) : null; } catch (error) { Logger2.warn("session-cache.read-failed", { name, error }); return null; } } function write(name, value) { try { sessionStorage.setItem(`${prefix}${name}`, JSON.stringify(value)); } catch (error) { Logger2.warn("session-cache.write-failed", { name, error }); } } function remove(name) { try { sessionStorage.removeItem(`${prefix}${name}`); } catch (error) { Logger2.warn("session-cache.remove-failed", { name, error }); } } function scopedName(scope, id) { return `${scope}:${encodeURIComponent(String(id))}`; } return { getSpaceTree(spaceId) { return read(scopedName("space-tree", spaceId)); }, saveSpaceTree(spaceId, value) { write(scopedName("space-tree", spaceId), value); }, clearSpaceTree(spaceId) { remove(scopedName("space-tree", spaceId)); }, getLabels(pageId) { return read(scopedName("labels", pageId)); }, saveLabels(pageId, value) { write(scopedName("labels", pageId), value); }, clearLabels(pageId) { remove(scopedName("labels", pageId)); } }; })(); return ContentSessionCache2; } // src/shared/theme.js var VALID_THEMES = ["light", "dark", "system"]; function resolveTheme(value, prefersDark, fallback = "system") { const theme = VALID_THEMES.includes(value) ? value : fallback; return theme === "system" ? prefersDark ? "dark" : "light" : theme; } function subscribeThemeChange(mediaQuery, handler) { if (!mediaQuery) return () => { }; if (typeof mediaQuery.addEventListener === "function") { mediaQuery.addEventListener("change", handler); return () => mediaQuery.removeEventListener("change", handler); } if (typeof mediaQuery.addListener === "function") { mediaQuery.addListener(handler); return () => mediaQuery.removeListener(handler); } return () => { }; } // src/shared/toast.js function createToastController({ state, selector, className, getLocaleText: getLocaleText2, closeLabelKey, getThemeClass, timerKey = "toastTimer", maxVisible = 3 }) { const ensureStack = () => { let stack = $(selector); if (!stack.length) stack = $(`[data-toast-stack].${className}-stack`); if (stack.length) return stack; stack = $(``).appendTo(document.body); return stack; }; const hideItem = (toast) => { toast.removeClass("is-visible is-success is-info is-error").addClass("is-leaving"); window.setTimeout(() => { toast.remove(); const stack = toast.parent(); if (!stack.children("[data-toast-item]").length) stack.prop("hidden", true); }, 180); }; const controller = { hide() { clearTimeout(state[timerKey]); state[timerKey] = -1; const stack = $(selector); stack.children("[data-toast-item]").remove(); stack.prop("hidden", true); }, show({ level = "info", title = "", message = "", duration = 5e3 } = {}) { const stack = ensureStack(); stack.toggleClass("is-dark", Boolean(getThemeClass?.())); const normalized = ["success", "info", "error"].includes(level) ? level : "info"; const toast = $(`
`); toast.addClass(`is-${normalized}`).find("[data-toast-title]").text(title || ""); toast.find("[data-toast-message]").text(message || ""); toast.find("[data-toast-time]").text((/* @__PURE__ */ new Date()).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit", hour12: false })); toast.find("[data-toast-icon]").text(normalized === "success" ? "\u2713" : normalized === "error" ? "!" : "i"); toast.find("[data-toast-close]").attr("aria-label", getLocaleText2(closeLabelKey)); toast.on("click.toast", "[data-toast-close]", () => hideItem(toast)); stack.prop("hidden", false).append(toast); const items = stack.children("[data-toast-item]"); if (items.length > maxVisible) items.slice(0, items.length - maxVisible).remove(); requestAnimationFrame(() => toast.addClass("is-visible")); clearTimeout(state[timerKey]); state[timerKey] = window.setTimeout(() => hideItem(toast), Math.max(1500, Number(duration) || 5e3)); } }; return controller; } // src/shared/feedback.js function createPopconfirmController({ getThemeClass, className = "shared-popconfirm" } = {}) { let active = null; const hide = () => active?.finish(false); const show = ({ anchor, title = "", message = "", confirmText = "Confirm", cancelText = "Cancel" } = {}) => new Promise((resolve) => { hide(); const trigger = anchor?.jquery ? anchor[0] : anchor; const rect = trigger?.getBoundingClientRect?.(); if (!rect) { resolve(false); return; } const wrapper = document.createElement("div"); wrapper.className = `${className}-wrap${getThemeClass?.() ? " is-dark" : ""}`; wrapper.innerHTML = ``; const popup = wrapper.firstElementChild; popup.querySelector(`.${className}-title`).textContent = title; const messageElement = popup.querySelector(`.${className}-message`); messageElement.textContent = message; messageElement.style.display = message ? "" : "none"; popup.querySelector(`.${className}-cancel`).textContent = cancelText; popup.querySelector(`.${className}-confirm`).textContent = confirmText; document.body.appendChild(wrapper); const popupRect = popup.getBoundingClientRect(); const left = Math.max(8, Math.min(rect.left + (rect.width - popupRect.width) / 2, window.innerWidth - popupRect.width - 8)); const showAbove = rect.top >= popupRect.height + 16; const top = showAbove ? rect.top - popupRect.height - 8 : Math.min(window.innerHeight - popupRect.height - 8, rect.bottom + 8); popup.classList.toggle("is-above", showAbove); popup.classList.toggle("is-below", !showAbove); wrapper.style.left = `${left}px`; wrapper.style.top = `${top}px`; let finished = false; const finish = (value) => { if (finished) return; finished = true; document.removeEventListener("keydown", onKeydown, true); document.removeEventListener("click", onOutside, true); wrapper.remove(); if (active?.wrapper === wrapper) active = null; resolve(value); }; const onKeydown = (event) => { if (event.key === "Escape") finish(false); else if (event.key === "Enter") finish(true); }; const onOutside = (event) => { if (!wrapper.contains(event.target)) finish(false); }; active = { finish, wrapper }; popup.querySelector(`.${className}-cancel`).addEventListener("click", () => finish(false)); popup.querySelector(`.${className}-confirm`).addEventListener("click", () => finish(true)); document.addEventListener("keydown", onKeydown, true); setTimeout(() => { if (!finished) document.addEventListener("click", onOutside, true); }, 0); popup.querySelector(`.${className}-confirm`).focus(); }); return { show, hide }; } // src/shared/tooltip.js var PLACEMENTS = ["right", "bottom", "left", "top"]; function positionTooltip(host, { tooltipSelector = '[role="tooltip"]', placementClassPrefix, gap = 10, fallbackWidth = 280, fallbackHeight = 48 } = {}) { const tooltip = host?.querySelector?.(tooltipSelector); if (!host || !tooltip || !placementClassPrefix) return null; const view = host.ownerDocument?.defaultView || globalThis.window; if (!view) return null; const hostRect = host.getBoundingClientRect(); const tooltipRect = tooltip.getBoundingClientRect(); const width = tooltipRect.width || fallbackWidth; const height = tooltipRect.height || fallbackHeight; const viewportPadding = 8; const spaces = { right: view.innerWidth - hostRect.right, bottom: view.innerHeight - hostRect.bottom, left: hostRect.left, top: hostRect.top }; const fits = { right: spaces.right >= width + gap && hostRect.top - height / 2 >= viewportPadding && hostRect.bottom + height / 2 <= view.innerHeight - viewportPadding, bottom: spaces.bottom >= height + gap && hostRect.right - width >= viewportPadding, left: spaces.left >= width + gap && hostRect.top - height / 2 >= viewportPadding && hostRect.bottom + height / 2 <= view.innerHeight - viewportPadding, top: spaces.top >= height + gap && hostRect.right - width >= viewportPadding }; const placement = PLACEMENTS.find((side) => fits[side]) || PLACEMENTS.find((side) => spaces[side] >= (side === "right" || side === "left" ? width : height) + gap) || "top"; tooltip.classList.remove(...PLACEMENTS.map((side) => placementClassPrefix + "--" + side)); tooltip.classList.add(placementClassPrefix + "--" + placement); return placement; } function bindTooltipPositioning(host, options) { if (!host) return () => { }; const reposition = () => positionTooltip(host, options); host.addEventListener("mouseenter", reposition); host.addEventListener("focusin", reposition); return () => { host.removeEventListener("mouseenter", reposition); host.removeEventListener("focusin", reposition); }; } var SHARED_TOOLTIP_STYLE_ID = "dft-shared-tooltip-styles"; var SHARED_TOOLTIP_HOST_CLASS = "dft-shared-tooltip-host"; var SHARED_TOOLTIP_CLASS = "dft-shared-tooltip"; var SHARED_TOOLTIP_CSS = [ ".dft-shared-tooltip-host { position: relative; }", ".dft-shared-tooltip { background: #172b4d; border-radius: 6px; box-shadow: 0 5px 16px rgba(9,30,66,.28); color: #fff; font: 12px/1.5 system-ui,sans-serif; max-width: min(360px,calc(100vw - 16px)); opacity: 0; padding: 8px 10px; pointer-events: none; position: fixed; text-align: left; transition: opacity .15s ease,visibility .15s ease; visibility: hidden; white-space: pre-line; width: max-content; z-index: 2147483647; }", ".dft-shared-tooltip.dft-shared-tooltip--visible { opacity: 1; visibility: visible; }", ".dft-shared-tooltip-host > .dft-shared-tooltip { background: #172b4d; border-radius: 6px; box-shadow: 0 5px 16px rgba(9,30,66,.28); color: #fff; font: 12px/1.5 system-ui,sans-serif; left: auto; max-width: min(360px,calc(100vw - 16px)); opacity: 0; padding: 8px 10px; pointer-events: none; position: absolute; text-align: left; top: calc(100% + 10px); transform: translateY(-4px); transition: opacity .15s ease,transform .15s ease,visibility .15s ease; visibility: hidden; white-space: pre-line; width: max-content; z-index: 100100; }", ".dft-shared-tooltip-host:hover > .dft-shared-tooltip, .dft-shared-tooltip-host:focus-within > .dft-shared-tooltip { opacity: 1; transform: translateY(0); visibility: visible; }", ".dft-shared-tooltip-host > .dft-shared-tooltip--right { left: calc(100% + 10px); top: 50%; transform: translateY(-50%) translateX(-4px); }", ".dft-shared-tooltip-host > .dft-shared-tooltip--bottom { left: auto; right: 0; top: calc(100% + 10px); transform: translateY(-4px); }", ".dft-shared-tooltip-host > .dft-shared-tooltip--left { left: auto; right: calc(100% + 10px); top: 50%; transform: translateY(-50%) translateX(4px); }", ".dft-shared-tooltip-host > .dft-shared-tooltip--top { bottom: calc(100% + 10px); left: auto; right: 0; top: auto; transform: translateY(4px); }", ".dft-shared-tooltip-host:hover > .dft-shared-tooltip--right, .dft-shared-tooltip-host:focus-within > .dft-shared-tooltip--right, .dft-shared-tooltip-host:hover > .dft-shared-tooltip--left, .dft-shared-tooltip-host:focus-within > .dft-shared-tooltip--left { transform: translateY(-50%) translateX(0); }", ".dft-shared-tooltip-host:hover > .dft-shared-tooltip--bottom, .dft-shared-tooltip-host:focus-within > .dft-shared-tooltip--bottom, .dft-shared-tooltip-host:hover > .dft-shared-tooltip--top, .dft-shared-tooltip-host:focus-within > .dft-shared-tooltip--top { transform: translateY(0); }" ].join("\n"); function installSharedTooltipStyles(document2) { if (!document2?.head || document2.getElementById(SHARED_TOOLTIP_STYLE_ID)) return; const style = document2.createElement("style"); style.id = SHARED_TOOLTIP_STYLE_ID; style.textContent = SHARED_TOOLTIP_CSS; document2.head.appendChild(style); } function isCustomTooltipElement(element) { return Boolean(element.closest?.([".ado-plus-action-tooltip-host", ".confluence-plus-label-title-tooltip", ".confluence-plus-label-apply-tooltip-host"].join(","))); } function positionSharedPortalTooltip(host, tooltip) { const view = host.ownerDocument?.defaultView || globalThis.window; if (!view) return; const rect = host.getBoundingClientRect(); tooltip.style.left = "-10000px"; tooltip.style.top = "-10000px"; tooltip.classList.add(SHARED_TOOLTIP_CLASS + "--visible"); const size = tooltip.getBoundingClientRect(); const width = size.width || 280; const height = size.height || 48; const gap = 10; const padding = 8; const spaces = { right: view.innerWidth - rect.right, bottom: view.innerHeight - rect.bottom, left: rect.left, top: rect.top }; const fits = { right: spaces.right >= width + gap && rect.top - height / 2 >= padding && rect.bottom + height / 2 <= view.innerHeight - padding, bottom: spaces.bottom >= height + gap && rect.right - width >= padding, left: spaces.left >= width + gap && rect.top - height / 2 >= padding && rect.bottom + height / 2 <= view.innerHeight - padding, top: spaces.top >= height + gap && rect.right - width >= padding }; const placement = PLACEMENTS.find((side) => fits[side]) || "top"; const positions = { right: [rect.right + gap, rect.top + (rect.height - height) / 2], bottom: [rect.right - width, rect.bottom + gap], left: [rect.left - width - gap, rect.top + (rect.height - height) / 2], top: [rect.right - width, rect.top - height - gap] }; const [left, top] = positions[placement]; tooltip.classList.remove(...PLACEMENTS.map((side) => SHARED_TOOLTIP_CLASS + "--" + side)); tooltip.classList.add(SHARED_TOOLTIP_CLASS + "--" + placement, SHARED_TOOLTIP_CLASS + "--visible"); tooltip.style.left = Math.max(padding, Math.min(left, view.innerWidth - width - padding)) + "px"; tooltip.style.top = Math.max(padding, Math.min(top, view.innerHeight - height - padding)) + "px"; } function setSharedTooltip(element, content) { if (!element || !content || element.tagName === "OPTION") return; installSharedTooltipStyles(element.ownerDocument); element.removeAttribute("title"); element.classList.add(SHARED_TOOLTIP_HOST_CLASS); let tooltip = element.__dftSharedTooltip; if (!tooltip) { tooltip = element.ownerDocument.createElement("span"); tooltip.className = SHARED_TOOLTIP_CLASS; tooltip.setAttribute("role", "tooltip"); element.__dftSharedTooltip = tooltip; (element.ownerDocument.body || element.ownerDocument.documentElement).appendChild(tooltip); } tooltip.textContent = String(content); if (!element.__dftSharedTooltipBound) { element.__dftSharedTooltipBound = true; const show = () => positionSharedPortalTooltip(element, tooltip); const hide = () => { tooltip.classList.remove(SHARED_TOOLTIP_CLASS + "--visible"); }; element.addEventListener("mouseenter", show); element.addEventListener("focusin", show); element.addEventListener("mouseleave", hide); element.addEventListener("focusout", hide); } } function convertNativeTitles(document2, root = document2) { if (!root?.querySelectorAll) return; const elements = []; if (root.nodeType === 1 && root.hasAttribute?.("title")) elements.push(root); elements.push(...root.querySelectorAll("[title]")); elements.forEach((element) => { const content = element.getAttribute("title"); if (!content) return; if (isCustomTooltipElement(element)) { element.removeAttribute("title"); return; } if (element.tagName === "OPTION") { const select = element.parentElement; if (select?.tagName === "SELECT" && select.selectedOptions[0] === element) setSharedTooltip(select.parentElement || select, content); element.removeAttribute("title"); return; } setSharedTooltip(element, content); }); } function installSharedTooltips(document2 = globalThis.document) { if (!document2 || document2.__dftSharedTooltipsInstalled) return; document2.__dftSharedTooltipsInstalled = true; installSharedTooltipStyles(document2); convertNativeTitles(document2); const observer = new MutationObserver((mutations) => mutations.forEach((mutation) => { if (mutation.type === "childList") mutation.addedNodes.forEach((node) => convertNativeTitles(document2, node)); if (mutation.type === "attributes" && mutation.attributeName === "title") convertNativeTitles(document2, mutation.target); })); observer.observe(document2.documentElement, { attributes: true, attributeFilter: ["title"], childList: true, subtree: true }); } // src/plugins/confluence/runtime/ui.js function createUI({ Config: Config2, Store: Store2, Utils: Utils2, Logger: Logger2, Settings: Settings2, AIConfig: AIConfig2, AzureOpenAI: AzureOpenAI2, ConfluenceApi: ConfluenceApi2, AILabelService: AILabelService2, SpaceTreeService: SpaceTreeService2, ContentSessionCache: ContentSessionCache2, getLocaleText: getLocaleText2, images: images2, windowIcons: windowIcons2 }) { const UI2 = (() => { const labelPopconfirm = createPopconfirmController({ getThemeClass: () => root?.classList.contains("confluence-plus-theme-dark"), className: "confluence-plus-popconfirm", eventNamespace: "confluencePlusPopconfirm" }); const confluenceToast = createToastController({ state: { toastTimer: -1 }, selector: "[data-confluence-toast-stack]", className: "confluence-plus-toast", getLocaleText: getLocaleText2, getThemeClass: () => root?.classList.contains("confluence-plus-theme-dark"), closeLabelKey: "close" }); let root; let unsubscribe; let overlay; let contextMenu; let adoStyleFab; let adoStyleConfig; let themeMediaQuery; let themeChangeHandler; let unsubscribeThemeChange; let contentFeatureUi = { spaceTreeOpen: false, aiLabelsOpen: false }; let lastViewport = { width: window.innerWidth, height: window.innerHeight }; function applyLayout() { if (!root) return; const ui = Store2.getUi(); root.classList.toggle("confluence-plus-expanded", !ui.minimized); root.style.position = "fixed"; root.style.zIndex = "999999"; if (ui.minimized) { root.style.left = "0px"; root.style.top = "0px"; root.style.right = "auto"; root.style.bottom = "auto"; return; } if (ui.maximized) { root.style.left = "0px"; root.style.top = "0px"; root.style.right = "auto"; root.style.bottom = "auto"; root.style.width = `${window.innerWidth}px`; root.style.height = `${window.innerHeight}px`; return; } root.style.width = `${ui.width}px`; root.style.height = `${ui.height}px`; root.style.right = "auto"; root.style.bottom = "auto"; root.style.left = `${ui.x}px`; root.style.top = `${ui.y}px`; } function centerPanel() { const ui = Store2.getUi(); const width = Math.min( ui.width, Math.max(Config2.ui.minWidth, window.innerWidth - 24) ); const height = Math.min( ui.height, Math.max(Config2.ui.minHeight, window.innerHeight - 24) ); const x = Math.max(0, Math.round((window.innerWidth - width) / 2)); const y = Math.max(0, Math.round((window.innerHeight - height) / 2)); Store2.updateUi({ x, y, width, height }); } function setMinimized(minimized) { Store2.updateUi({ minimized }); render(); } function updateWindowChrome() { if (!root) return; applyLayout(); const button = root.querySelector("#confluence-plus-toggle-maximize"); if (!button) return; const maximized = Store2.getUi().maximized; button.innerHTML = maximized ? windowIcons2.restore : windowIcons2.maximize; button.title = getLocaleText2(maximized ? "restoreWindow" : "maximizeWindow"); button.setAttribute( "aria-label", getLocaleText2(maximized ? "restoreWindow" : "maximizeWindow") ); } function toggleMaximized() { const ui = Store2.getUi(); if (ui.maximized) { Store2.updateUi({ maximized: false, width: ui._normalWidth || ui.width, height: ui._normalHeight || ui.height, x: ui._normalX ?? ui.x, y: ui._normalY ?? ui.y }); } else { Store2.updateUi({ maximized: true, _normalWidth: ui.width, _normalHeight: ui.height, _normalX: ui.x, _normalY: ui.y }); } updateWindowChrome(); } function showMenuOverlay() { if (!overlay) { overlay = document.createElement("div"); overlay.id = "confluence-plus-menu-overlay"; overlay.addEventListener("click", () => { setMinimized(true); }); document.body.appendChild(overlay); } overlay.style.display = "block"; } function hideMenuOverlay() { if (overlay) { overlay.style.display = "none"; } } function getCurrentSpaceIconUrl() { const icon = document.querySelector( 'img[data-testid="space-icon"], img[data-vc="space-header-icon-image"], [data-testid="space-icon"]' ); const source = icon?.currentSrc || icon?.src || icon?.getAttribute?.("src") || ""; if (!source) return images2.plugin; try { return new URL(source, location.href).href; } catch { return source; } } function applyContentWindowSpaceIcon() { const icon = root?.querySelector("[data-confluence-plus-space-icon]"); if (!icon) return; const fallback = images2.plugin; icon.src = getCurrentSpaceIconUrl() || fallback; icon.alt = getLocaleText2("currentSpaceIcon"); icon.onerror = () => { if (icon.src !== fallback) icon.src = fallback; }; } function renderFloatingButtonLocale() { const tooltip = adoStyleFab?.querySelector(".confluence-plus-tooltip"); if (!tooltip) return; tooltip.innerHTML = `${getLocaleText2("leftClick")}
${getLocaleText2("rightClick")}`; } function applyFloatingButtonPosition() { if (!adoStyleFab) return; adoStyleFab.style.right = `${Settings2.get().general.floatingButtonRight}px`; renderFloatingButtonLocale(); } function applyConfigTheme() { const settings = Settings2.get(); const isDark = resolveTheme( settings.general.theme, Boolean(window.matchMedia?.("(prefers-color-scheme: dark)").matches) ) === "dark"; const themeClass = isDark ? "confluence-plus-theme-dark" : "confluence-plus-theme-light"; [root, adoStyleFab, adoStyleConfig].forEach((element) => { element?.classList.remove( "confluence-plus-theme", "confluence-plus-theme-light", "confluence-plus-theme-dark" ); element?.classList.add("confluence-plus-theme", themeClass); }); } function bindThemeListener() { if (!window.matchMedia || themeMediaQuery) return; themeMediaQuery = window.matchMedia("(prefers-color-scheme: dark)"); themeChangeHandler = () => { if (Settings2.get().general.theme === "system") { applyConfigTheme(); } }; unsubscribeThemeChange = subscribeThemeChange(themeMediaQuery, themeChangeHandler); } function unbindThemeListener() { if (!themeMediaQuery || !themeChangeHandler) return; unsubscribeThemeChange?.(); unsubscribeThemeChange = null; themeMediaQuery = null; themeChangeHandler = null; } function closeAdoStyleConfig() { if (!adoStyleConfig) return; adoStyleConfig.hidden = true; adoStyleConfig.setAttribute("aria-hidden", "true"); } function getContentAiSummaryStatus() { const config = AIConfig2.get(); const configured = Boolean( config.endpoint.trim() && config.apiKey.trim() && config.deployment.trim() ); return { key: configured ? "ai-status-available" : "ai-status-unconfigured", label: configured ? getLocaleText2("aiStatusAvailable") : getLocaleText2("aiStatusNotConfigured") }; } function renderContentAiDeployments() { const options = adoStyleConfig?.querySelector("#confluencePlusContentAiDeploymentOptions"); if (!options) return; options.innerHTML = ""; AIConfig2.get().deployments.forEach((deployment) => { if (!deployment?.id) return; const option = document.createElement("option"); option.value = deployment.id; option.label = deployment.label || deployment.id; options.appendChild(option); }); } function getContentAiConnectionMessage(check) { if (!check) return getLocaleText2("notTested"); if (check.status === "success" && Number.isFinite(check.deploymentCount)) { return getLocaleText2("deploymentVerified"); } return check.message || getLocaleText2("notTested"); } function refreshContentAiSummary() { const summary = adoStyleConfig?.querySelector("#confluencePlusContentAiStatus"); if (!summary) return; const status = getContentAiSummaryStatus(); summary.className = `confluence-plus-ai-status ${status.key}`; summary.textContent = status.label; } function bindContentAiControls() { if (!adoStyleConfig) return; const endpoint = adoStyleConfig.querySelector("#confluencePlusContentAiEndpoint"); const apiKey = adoStyleConfig.querySelector("#confluencePlusContentAiKey"); const apiVersion = adoStyleConfig.querySelector("#confluencePlusContentAiApiVersion"); const deployment = adoStyleConfig.querySelector("#confluencePlusContentAiDeployment"); const tokenParameter = adoStyleConfig.querySelector("#confluencePlusContentAiTokenParameter"); const result = adoStyleConfig.querySelector("#confluencePlusContentAiResult"); const testButton = adoStyleConfig.querySelector("[data-content-ai-test]"); const clearButton = adoStyleConfig.querySelector("[data-content-ai-clear]"); const showKeyButton = adoStyleConfig.querySelector("[data-content-ai-show-key]"); const setResult = (message, status = "") => { result.className = `confluence-plus-ai-result${status ? ` ai-result-${status}` : ""}`; result.textContent = message; }; const current = AIConfig2.get(); endpoint.value = current.endpoint; apiKey.value = current.apiKey; apiVersion.value = current.apiVersion || "2025-03-01-preview"; deployment.value = current.deployment; tokenParameter.value = current.tokenParameter || "max_completion_tokens"; renderContentAiDeployments(); if (current.lastCheck) { setResult( getContentAiConnectionMessage({ ...current.lastCheck, status: current.lastCheck.ok ? "success" : "error" }), current.lastCheck.ok ? "success" : "error" ); } const updateDraft = () => { AIConfig2.updateDraft({ endpoint: endpoint.value, apiKey: apiKey.value, apiVersion: apiVersion.value, deployment: deployment.value, tokenParameter: tokenParameter.value }); refreshContentAiSummary(); }; [endpoint, apiKey, apiVersion, deployment, tokenParameter].forEach((field) => { field.addEventListener("input", updateDraft); field.addEventListener("change", updateDraft); }); showKeyButton.addEventListener("click", () => { const visible = apiKey.type === "text"; apiKey.type = visible ? "password" : "text"; showKeyButton.textContent = getLocaleText2(visible ? "show" : "hide"); }); clearButton.addEventListener("click", () => { AIConfig2.clear(); endpoint.value = ""; apiKey.value = ""; apiVersion.value = "2025-03-01-preview"; deployment.value = ""; tokenParameter.value = "max_completion_tokens"; renderContentAiDeployments(); setResult(getLocaleText2("notTested")); refreshContentAiSummary(); }); testButton.addEventListener("click", async () => { updateDraft(); const draft = AIConfig2.get(); if (!draft.endpoint || !draft.apiKey) { setResult(getLocaleText2("missingEndpointOrKey"), "error"); return; } testButton.disabled = true; setResult(getLocaleText2("connecting")); try { const candidate = AIConfig2.validate(draft); const connection = await AzureOpenAI2.testConnection(candidate); const selectedDeployment = deployment.value.trim() || connection.deployments[0]?.id || ""; AIConfig2.updateDraft({ ...candidate, deployment: selectedDeployment, deployments: connection.deployments }); AIConfig2.recordHealth(true, connection.message, { deploymentCount: connection.deploymentCount, v1: AzureOpenAI2.isV1Endpoint(candidate.endpoint) }); deployment.value = selectedDeployment; renderContentAiDeployments(); setResult(connection.message, "success"); refreshContentAiSummary(); } catch (error) { AIConfig2.recordHealth(false, error.message); setResult(error.message, "error"); refreshContentAiSummary(); } finally { testButton.disabled = false; } }); } function renderLocale() { if (!adoStyleConfig) return; adoStyleConfig.querySelectorAll("[data-confluence-plus-locale]").forEach((element) => { const key = element.dataset.confluencePlusLocale; const attribute = element.dataset.confluencePlusLocaleAttr; if (attribute) { element.setAttribute(attribute, getLocaleText2(key)); } else { element.textContent = getLocaleText2(key); } }); const title = adoStyleConfig.querySelector("#confluencePlusConfigTitle"); if (title) { title.textContent = `${getLocaleText2("settings")} (v${GM_info.script.version})`; } adoStyleConfig.querySelectorAll("[data-confluence-plus-option-locale]").forEach((option) => { option.textContent = getLocaleText2(option.dataset.confluencePlusOptionLocale); }); const language = adoStyleConfig.querySelector("#confluencePlusConfigLanguage"); const theme = adoStyleConfig.querySelector("#confluencePlusConfigTheme"); if (language) language.setAttribute("aria-label", getLocaleText2("language")); if (theme) theme.setAttribute("aria-label", getLocaleText2("theme")); const showKey = adoStyleConfig.querySelector("[data-content-ai-show-key]"); if (showKey) { const keyInput = adoStyleConfig.querySelector("#confluencePlusContentAiKey"); showKey.textContent = getLocaleText2(keyInput?.type === "text" ? "hide" : "show"); } const current = AIConfig2.get(); const result = adoStyleConfig.querySelector("#confluencePlusContentAiResult"); if (result && current.lastCheck) { result.textContent = getContentAiConnectionMessage({ ...current.lastCheck, status: current.lastCheck.ok ? "success" : "error" }); result.removeAttribute("data-confluence-plus-locale"); } } function renderAdoStyleConfig() { if (!adoStyleConfig) return; const language = Settings2.get().general.language; const theme = Settings2.get().general.theme; const right = Settings2.get().general.floatingButtonRight; const aiConfig = AIConfig2.get(); const aiStatus = getContentAiSummaryStatus(); const option = (value, labelKey) => ``; const themeOption = (value, labelKey) => ``; adoStyleConfig.innerHTML = `
`; adoStyleConfig.querySelectorAll("[data-confluence-plus-ado-close]").forEach((element) => { element.addEventListener("click", closeAdoStyleConfig); }); adoStyleConfig.querySelector("#confluencePlusConfigLanguage")?.addEventListener( "change", (event) => { Settings2.updateGeneral("language", event.currentTarget.value); renderLocale(); applyConfigTheme(); renderFloatingButtonLocale(); if (!Store2.getUi().minimized) { render(true); } console.log( "[Confluence Plus] Configuration dialog language-updated HTML:\n" + adoStyleConfig.outerHTML ); } ); adoStyleConfig.querySelector("#confluencePlusConfigTheme")?.addEventListener( "change", (event) => { Settings2.updateGeneral("theme", event.currentTarget.value); renderLocale(); applyConfigTheme(); console.log( "[Confluence Plus] Configuration dialog theme-updated HTML:\n" + adoStyleConfig.outerHTML ); } ); adoStyleConfig.querySelector("#confluencePlusConfigFloatingRight")?.addEventListener( "change", (event) => { Settings2.updateGeneral("floatingButtonRight", event.currentTarget.value); applyFloatingButtonPosition(); event.currentTarget.value = Settings2.get().general.floatingButtonRight; } ); bindContentAiControls(); applyConfigTheme(); renderFloatingButtonLocale(); console.log( "[Confluence Plus] Configuration dialog rendered HTML:\n" + adoStyleConfig.outerHTML ); } function openAdoStyleConfig() { if (!adoStyleConfig) return; renderAdoStyleConfig(); adoStyleConfig.hidden = false; adoStyleConfig.setAttribute("aria-hidden", "false"); applyConfigTheme(); adoStyleConfig.querySelector(".confluence-plus-ado-modal-close")?.focus(); } function createAdoStyleEntry() { if (adoStyleFab) return; adoStyleFab = document.createElement("div"); adoStyleFab.id = "confluencePlusStart"; adoStyleFab.className = "confluence-plus-floating-icon confluence-plus-tooltip-host"; adoStyleFab.setAttribute("aria-label", "Confluence Plus"); adoStyleFab.innerHTML = ` Confluence Plus Left-click: Open feature window
Right-click: Open configuration window
`; adoStyleFab.addEventListener("click", (event) => { if (event.button === 0) openMenu(); }); adoStyleFab.addEventListener("contextmenu", (event) => { event.preventDefault(); event.stopPropagation(); openAdoStyleConfig(); }); document.body.appendChild(adoStyleFab); bindTooltipPositioning(adoStyleFab, { tooltipSelector: ".confluence-plus-tooltip", placementClassPrefix: "confluence-plus-tooltip" }); adoStyleConfig = document.createElement("div"); adoStyleConfig.id = "confluencePlusConfigDialog"; adoStyleConfig.className = "confluence-plus-ado-modal"; adoStyleConfig.hidden = true; adoStyleConfig.setAttribute("aria-hidden", "true"); document.body.appendChild(adoStyleConfig); bindThemeListener(); applyFloatingButtonPosition(); applyConfigTheme(); } function removeAdoStyleEntry() { closeAdoStyleConfig(); adoStyleConfig?.remove(); adoStyleFab?.remove(); adoStyleConfig = null; adoStyleFab = null; } function renderSpaceTreePage(container) { const target = container; const isBlogPostRoute = Utils2.isBlogPostUrl(location.href); let pageId = isBlogPostRoute ? "" : Store2.getContext().pageId; const treeCurrentPageId = pageId; target.innerHTML = `
${getLocaleText2("loadingSpaceTree")}
`; const status = target.querySelector("#confluence-plus-tree-status"); const treeContainer = target.querySelector("#confluence-plus-space-tree"); let treeState = null; let currentSpaceId = ""; let currentSpaceKey = ""; let currentSpaceName = ""; let loadedPages = []; function persistTreeCache() { if (!currentSpaceId || !treeState) return; ContentSessionCache2.saveSpaceTree(currentSpaceId, { version: 1, spaceId: currentSpaceId, spaceKey: currentSpaceKey, spaceName: currentSpaceName, pages: loadedPages, expandedIds: [...treeState.expanded], loadedAt: Date.now() }); } function renderNodes(nodes) { const list = document.createElement("ul"); list.className = "confluence-plus-tree-list"; nodes.forEach((node) => { const item = document.createElement("li"); item.className = "confluence-plus-tree-item"; item.dataset.pageId = node.id; item.dataset.spaceKey = currentSpaceKey; item.dataset.nodeType = node.page?.type || "page"; const row = document.createElement("div"); row.className = "confluence-plus-tree-row"; if (node.id === String(treeCurrentPageId)) { row.classList.add("confluence-plus-tree-current"); row.dataset.treeCurrent = "1"; } if (node.children.length) { const toggle = document.createElement("button"); const isExpanded = treeState.expanded.has(node.id); toggle.className = "confluence-plus-tree-toggle"; toggle.innerText = isExpanded ? "\u25BE" : "\u25B8"; toggle.title = isExpanded ? getLocaleText2("collapseChildren") : getLocaleText2("expandChildren"); toggle.addEventListener("click", () => { if (treeState.expanded.has(node.id)) { treeState.expanded.delete(node.id); } else { treeState.expanded.add(node.id); } persistTreeCache(); renderTree(true); }); row.appendChild(toggle); } else { const spacer = document.createElement("span"); spacer.className = "confluence-plus-tree-toggle-spacer"; row.appendChild(spacer); } const titleWrap = document.createElement("span"); titleWrap.className = "confluence-plus-tree-title-wrap"; const title = document.createElement("span"); title.className = "confluence-plus-tree-title"; title.innerText = node.title; title.title = `${node.title} (${node.id})`; titleWrap.appendChild(title); if (node.children.length > 0) { const childCount = document.createElement("span"); childCount.className = "confluence-plus-tree-child-count"; childCount.innerText = `(${node.children.length})`; childCount.title = getLocaleText2("childPages", { count: node.children.length }); titleWrap.appendChild(childCount); } if (node.id === String(treeCurrentPageId)) { const badge = document.createElement("span"); badge.className = "confluence-plus-tree-current-badge"; badge.innerText = getLocaleText2("currentPageBadge"); titleWrap.appendChild(badge); } row.appendChild(titleWrap); item.appendChild(row); if (node.children.length && treeState.expanded.has(node.id)) { item.appendChild(renderNodes(node.children)); } list.appendChild(item); }); return list; } function renderTree(preserveScroll = false) { const scrollTop = preserveScroll ? treeContainer.scrollTop : 0; treeContainer.innerHTML = ""; if (!treeState.roots.length) { treeContainer.innerText = getLocaleText2("noPagesInSpace"); return; } treeContainer.appendChild(renderNodes(treeState.roots)); if (preserveScroll) { treeContainer.scrollTop = scrollTop; return; } const current = treeContainer.querySelector( '[data-tree-current="1"]' ); current?.scrollIntoView({ block: "center", behavior: "smooth" }); } async function loadTree(forceRefresh = false) { status.className = "confluence-plus-modal-status"; status.innerText = getLocaleText2("loadingSpaceTree"); treeContainer.innerHTML = ""; try { let space = null; if (!Utils2.isValidPageId(pageId)) { const spaceMatch = location.pathname.match( /\/spaces\/([^/]+)/i ); const spaceKeyFromUrl = spaceMatch ? decodeURIComponent(spaceMatch[1]) : ""; if (spaceKeyFromUrl) { space = await ConfluenceApi2.spaces.getByKey( spaceKeyFromUrl ); pageId = String( space?.homepageId || space?.homepage?.id || "" ); } } if (!Utils2.isValidPageId(pageId)) { throw new Error(getLocaleText2("invalidPageId")); } const currentPage = await ConfluenceApi2.pages.get(pageId); const spaceId = currentPage.spaceId || space?.id; if (!spaceId) { throw new Error(getLocaleText2("missingSpaceId")); } if (!space || String(space.id) !== String(spaceId)) { space = await ConfluenceApi2.spaces.get(spaceId); } if (!isBlogPostRoute) { Store2.setContext({ pageId }); } const spaceName = (space.name || "").trim(); currentSpaceId = String(spaceId); currentSpaceName = spaceName; currentSpaceKey = (space.key || "").trim(); if (!spaceName || !currentSpaceKey) { throw new Error(getLocaleText2("missingSpaceInfo")); } const cached = forceRefresh ? null : ContentSessionCache2.getSpaceTree(currentSpaceId); if (forceRefresh) { ContentSessionCache2.clearSpaceTree(currentSpaceId); } loadedPages = Array.isArray(cached?.pages) ? cached.pages : await SpaceTreeService2.load(spaceId); treeState = SpaceTreeService2.buildTree( loadedPages, treeCurrentPageId, spaceName ); renderTree(); persistTreeCache(); status.className = "confluence-plus-modal-status success"; status.innerText = cached ? getLocaleText2("loadedPagesFromCache", { count: treeState.filteredCount }) : getLocaleText2("loadedPages", { count: treeState.filteredCount }); } catch (error) { status.className = "confluence-plus-modal-status error"; status.innerText = error.message; Logger2.error("space-tree.load-failed", error); } } target.querySelector("#confluence-plus-tree-refresh").addEventListener( "click", () => loadTree(true) ); loadTree(); } function renderAiLabelsPage(container) { const target = container; const pageId = Store2.getContext().pageId; if (!Utils2.isValidPageId(pageId)) { target.innerHTML = '
' + getLocaleText2("invalidPageId") + "
"; return; } target.innerHTML = `
${getLocaleText2("generateSuggestionsForPage")}
${getLocaleText2("pageContentSentToAi")}
`; const status = target.querySelector("#confluence-plus-label-status"); const results = target.querySelector("#confluence-plus-label-results"); const generateButton = target.querySelector("#confluence-plus-label-generate"); generateButton.disabled = AIConfig2.getStatus().key !== "available"; const cachedLabelState = ContentSessionCache2.getLabels(pageId); let currentPageTitle = ""; let existingLabels = Array.isArray(cachedLabelState?.existingLabels) ? [...cachedLabelState.existingLabels] : []; function persistLabelState() { const rows = [...target.querySelectorAll(".confluence-plus-label-item")]; ContentSessionCache2.saveLabels(pageId, { version: 1, pageId: String(pageId), existingLabels: [...existingLabels], suggestions: rows.map((row) => ({ value: row.querySelector(".confluence-plus-label-edit")?.value || "", checked: Boolean(row.querySelector('input[type="checkbox"]')?.checked) })), manualDraft: target.querySelector("#confluence-plus-label-manual")?.value || "", status: { className: status.className, message: status.innerText }, updatedAt: Date.now() }); } function setLabelStatus(message, type = "") { status.className = `confluence-plus-modal-status${type ? ` ${type}` : ""}`; status.innerText = message; persistLabelState(); } results.addEventListener("input", persistLabelState); results.addEventListener("change", persistLabelState); function renderSuggestions(suggestions, pageTitle = currentPageTitle) { results.innerHTML = `
`; const resultsHeader = results.querySelector( "[data-confluence-plus-label-title-tooltip]" ); const headerText = pageTitle ? getLocaleText2("recommendedLabelsFor", { title: pageTitle }) : getLocaleText2("recommendedLabels"); resultsHeader.querySelector(".confluence-plus-label-results-header-text").textContent = headerText; resultsHeader.dataset.confluencePlusTooltip = pageTitle || headerText; resultsHeader.title = pageTitle || headerText; const list = results.querySelector("#confluence-plus-label-list"); const applyButton = results.querySelector("#confluence-plus-label-apply"); function getApplicableLabels() { const selected = [...list.querySelectorAll(".confluence-plus-label-item")].filter((row) => row.querySelector('input[type="checkbox"]').checked).map((row) => AILabelService2.normalize( row.querySelector(".confluence-plus-label-edit").value )).filter(Boolean); const existing = new Set( existingLabels.map((label) => AILabelService2.normalize(label)) ); return [...new Set(selected)].filter((label) => !existing.has(label)); } function updateApplyButton() { const canApply = getApplicableLabels().length > 0; applyButton.disabled = !canApply; applyButton.title = canApply ? getLocaleText2("applySelectedLabelsTitle") : getLocaleText2("noLabelsToApplyTooltip"); applyButton.parentElement.dataset.tooltip = applyButton.title; applyButton.parentElement.setAttribute("aria-label", applyButton.title); applyButton.setAttribute("aria-disabled", String(!canApply)); } function appendLabel(label, checked = true) { const normalized = AILabelService2.normalize(label); if (!normalized || list.querySelector( `[data-label="${CSS.escape(normalized)}"]` )) { return; } const row = document.createElement("label"); row.className = "confluence-plus-label-item"; row.dataset.label = normalized; row.innerHTML = ` `; row.querySelector(".confluence-plus-label-remove").addEventListener( "click", () => { row.remove(); persistLabelState(); updateApplyButton(); } ); list.appendChild(row); } suggestions.forEach((suggestion) => { const value = typeof suggestion === "string" ? suggestion : suggestion?.value; const checked = typeof suggestion === "string" ? true : suggestion?.checked !== false; appendLabel(value, checked); }); updateApplyButton(); results.querySelector("#confluence-plus-label-add-manual").addEventListener( "click", () => { const input = results.querySelector("#confluence-plus-label-manual"); appendLabel(input.value); input.value = ""; persistLabelState(); updateApplyButton(); } ); results.addEventListener("input", updateApplyButton); results.addEventListener("change", updateApplyButton); function showLabelConfirmation(labels, anchor) { return labelPopconfirm.show({ anchor, title: getLocaleText2(labels.length === 1 ? "applyOneLabelQuestion" : "applyManyLabelsQuestion", void 0, { count: labels.length }), message: "", confirmText: getLocaleText2("apply"), cancelText: getLocaleText2("cancel") }); } results.querySelector("#confluence-plus-label-apply").addEventListener( "click", async () => { const labels = getApplicableLabels(); if (!labels.length) { setLabelStatus(getLocaleText2("selectNewValidLabel"), "error"); updateApplyButton(); return; } const confirmed = await showLabelConfirmation( labels, applyButton ); if (!confirmed) { return; } applyButton.disabled = true; setLabelStatus(getLocaleText2("applyingLabels")); try { await ConfluenceApi2.pages.addLabels(pageId, labels); existingLabels.push(...labels); applyButton.disabled = true; setLabelStatus( getLocaleText2("addedLabelsRefreshing", { count: labels.length }), "success" ); ContentSessionCache2.clearLabels(pageId); Logger2.info("ai-labels.applied", { pageId, labels }); setTimeout(() => location.reload(), 800); } catch (error) { setLabelStatus(error.message, "error"); Logger2.error("ai-labels.apply-failed", error); applyButton.disabled = false; } } ); } function localizeCachedLabelStatus(message) { const generated = String(message || "").match(/^Generated (\d+) suggestion\(s\)$/); return generated ? getLocaleText2("generatedSuggestions", { count: Number(generated[1]) }) : message; } if (cachedLabelState) { if (Array.isArray(cachedLabelState.suggestions)) { renderSuggestions(cachedLabelState.suggestions); } const manualDraft = target.querySelector("#confluence-plus-label-manual"); if (manualDraft) { manualDraft.value = cachedLabelState.manualDraft || ""; } if (cachedLabelState.status?.message) { status.className = cachedLabelState.status.className || "confluence-plus-modal-status"; status.innerText = localizeCachedLabelStatus(cachedLabelState.status.message); } } generateButton.addEventListener("click", async () => { if (!AIConfig2.isConfigured()) { setLabelStatus(getLocaleText2("configureAiBeforeLabels"), "error"); return; } generateButton.disabled = true; setLabelStatus(getLocaleText2("loadingAndGenerating")); results.innerHTML = ""; persistLabelState(); try { const result = await AILabelService2.generate(pageId); existingLabels = Array.isArray(result.existingLabels) ? [...result.existingLabels] : []; currentPageTitle = result.page.title || getLocaleText2("currentPageBadge"); renderSuggestions(result.suggestions, currentPageTitle); setLabelStatus( result.suggestions.length ? getLocaleText2("generatedSuggestions", { count: result.suggestions.length }) : getLocaleText2("noNewLabelSuggestions"), "success" ); persistLabelState(); Logger2.info("ai-labels.generated", { pageId, count: result.suggestions.length }); } catch (error) { setLabelStatus(error.message, "error"); Logger2.error("ai-labels.generate-failed", error); } finally { generateButton.disabled = false; } }); } function openMenu() { const ui = Store2.getUi(); Store2.updateUi({ width: Math.max(ui.width, 720), height: Math.max(ui.height, 520) }); centerPanel(); Store2.updateUi({ minimized: false }); render(); } function renderMenu() { const maximized = Store2.getUi().maximized; root.innerHTML = `
${getLocaleText2("contentToolTitle")} ${getLocaleText2("contentToolSubtitle")}
${getLocaleText2("spacePageTree")} ${getLocaleText2("spacePageTreeDescription")}
${getLocaleText2("generateLabel")} ${getLocaleText2("aiConfigurationRequired")} ${getLocaleText2("generateLabelDescription")}
`; applyContentWindowSpaceIcon(); root.querySelector("#confluence-plus-close-btn").innerHTML = windowIcons2.close; root.querySelector("#confluence-plus-close-btn").addEventListener( "click", () => setMinimized(true) ); const spaceTreeFeature = root.querySelector("#confluence-plus-space-tree-feature"); const aiLabelFeature = root.querySelector("#confluence-plus-ai-label-feature"); const spaceTreeHost = root.querySelector("[data-confluence-plus-space-tree-host]"); const aiLabelHost = root.querySelector("[data-confluence-plus-ai-label-host]"); const mountSpaceTree = () => { if (!spaceTreeFeature.open || spaceTreeFeature.dataset.loaded) return; spaceTreeFeature.dataset.loaded = "true"; renderSpaceTreePage(spaceTreeHost); }; const mountAiLabels = () => { if (!aiLabelFeature.open || aiLabelFeature.dataset.loaded) return; aiLabelFeature.dataset.loaded = "true"; renderAiLabelsPage(aiLabelHost); }; spaceTreeFeature.addEventListener("toggle", () => { contentFeatureUi.spaceTreeOpen = spaceTreeFeature.open; mountSpaceTree(); }); aiLabelFeature.addEventListener("toggle", () => { contentFeatureUi.aiLabelsOpen = aiLabelFeature.open; mountAiLabels(); }); const featureUi = contentFeatureUi; if (featureUi.spaceTreeOpen) { spaceTreeFeature.open = true; mountSpaceTree(); } if (featureUi.aiLabelsOpen) { aiLabelFeature.open = true; mountAiLabels(); } } function protectHeaderControls() { root.querySelectorAll( ".confluence-plus-header-actions, .confluence-plus-header button" ).forEach((element) => { ["mousedown", "pointerdown", "touchstart"].forEach((eventName) => { element.addEventListener(eventName, (event) => { event.stopPropagation(); }); }); }); } function render(preserveLayout = false) { if (!root) return; const layoutBeforeRender = preserveLayout ? { ...Store2.getUi() } : null; const isContentWindow = !Store2.getUi().minimized; root.classList.toggle("confluence-plus-content-window", isContentWindow); if (isContentWindow) { root.setAttribute("role", "dialog"); root.setAttribute("aria-modal", "true"); root.setAttribute("aria-labelledby", "confluence-plus-content-window-title"); } else { root.removeAttribute("role"); root.removeAttribute("aria-modal"); root.removeAttribute("aria-labelledby"); } applyConfigTheme(); applyLayout(); if (Store2.getUi().minimized) { hideMenuOverlay(); root.hidden = true; root.setAttribute("aria-hidden", "true"); return; } root.hidden = false; root.removeAttribute("aria-hidden"); showMenuOverlay(); renderMenu(); protectHeaderControls(); root.querySelector("#confluence-plus-toggle-maximize")?.addEventListener( "click", toggleMaximized ); if (layoutBeforeRender) { Store2.updateUi({ x: layoutBeforeRender.x, y: layoutBeforeRender.y, width: layoutBeforeRender.width, height: layoutBeforeRender.height, maximized: layoutBeforeRender.maximized }); applyLayout(); } } function updatePageId() { const input = root?.querySelector("#confluence-plus-current-page-id"); if (input) { input.value = Store2.getContext().pageId; } } function toast(message, type = "info") { confluenceToast.show({ level: type === "error" ? "error" : type === "success" ? "success" : "info", message }); } function hideContextMenu() { contextMenu?.remove(); contextMenu = null; } function showContextMenu(event) { if (Store2.getUi().minimized) return; const target = event.target && typeof event.target.closest === "function" ? event.target : null; const treeRow = target?.closest(".confluence-plus-tree-row"); const treeItem = treeRow?.closest(".confluence-plus-tree-item"); if (!treeRow || !treeItem || !root?.contains(treeRow)) return; event.preventDefault(); event.stopPropagation(); hideContextMenu(); const script = typeof GM_info !== "undefined" ? GM_info.script || {} : {}; const name = script.name || "Confluence Plus"; const version = script.version || "unknown"; const pageId = treeItem?.dataset.pageId || ""; const spaceKey = treeItem?.dataset.spaceKey || ""; const nodeType = treeItem?.dataset.nodeType || "page"; const canOpenPage = Boolean( pageId && spaceKey && nodeType !== "folder" ); const menuWidth = 220; const menuHeight = canOpenPage ? 92 : 54; const left = Math.min( Math.max(8, event.clientX), window.innerWidth - menuWidth - 8 ); const top = Math.min( Math.max(8, event.clientY), window.innerHeight - menuHeight - 8 ); contextMenu = document.createElement("div"); contextMenu.className = "confluence-plus-context-menu"; contextMenu.setAttribute("role", "menu"); contextMenu.style.left = `${left}px`; contextMenu.style.top = `${top}px`; if (canOpenPage) { const openPage = document.createElement("button"); openPage.type = "button"; openPage.className = "confluence-plus-context-menu-action"; openPage.setAttribute("role", "menuitem"); openPage.textContent = getLocaleText2("openPage"); openPage.addEventListener("click", (clickEvent) => { clickEvent.preventDefault(); clickEvent.stopPropagation(); hideContextMenu(); const pageUrl = `${location.origin}/wiki/spaces/${encodeURIComponent(spaceKey)}/pages/${pageId}`; window.open(pageUrl, "_blank", "noopener,noreferrer"); }); contextMenu.appendChild(openPage); } const item = document.createElement("div"); item.className = "confluence-plus-context-menu-item confluence-plus-context-menu-info"; item.setAttribute("role", "menuitem"); item.setAttribute("aria-disabled", "true"); item.textContent = `${name} v${version}`; contextMenu.appendChild(item); document.body.appendChild(contextMenu); setTimeout(() => { document.addEventListener("click", hideContextMenu, { once: true, capture: true }); }, 0); } function enableDrag() { let dragging = false; let offsetX = 0; let offsetY = 0; root.addEventListener("dblclick", (event) => { if (Store2.getUi().minimized || event.button !== 0) return; const header = event.target.closest(".confluence-plus-header"); const interactive = event.target.closest( "button, input, select, textarea, a, .confluence-plus-header-actions" ); if (!header || interactive) return; event.preventDefault(); event.stopPropagation(); toggleMaximized(); }); root.addEventListener("mousedown", (event) => { if (Store2.getUi().minimized || Store2.getUi().maximized || event.button !== 0) return; const header = event.target.closest(".confluence-plus-header"); const interactive = event.target.closest( "button, input, select, textarea, a, .confluence-plus-header-actions" ); if (!header || interactive) { return; } const rect = root.getBoundingClientRect(); dragging = true; offsetX = event.clientX - rect.left; offsetY = event.clientY - rect.top; document.body.style.userSelect = "none"; }); document.addEventListener("mousemove", (event) => { if (!dragging) return; const rect = root.getBoundingClientRect(); const maxX = Math.max(0, window.innerWidth - rect.width); const maxY = Math.max(0, window.innerHeight - rect.height); const x = Math.min( maxX, Math.max(0, event.clientX - offsetX) ); const y = Math.min( maxY, Math.max(0, event.clientY - offsetY) ); root.style.left = `${x}px`; root.style.top = `${y}px`; root.style.right = "auto"; root.style.bottom = "auto"; Store2.updateUi({ x, y }); }); document.addEventListener("mouseup", () => { if (!dragging) return; dragging = false; document.body.style.userSelect = ""; }); } function observeResize() { if (!window.ResizeObserver) return; const observer = new ResizeObserver( Utils2.debounce(() => { if (!root || Store2.getUi().minimized) return; const ui = Store2.getUi(); const rect = root.getBoundingClientRect(); const width = Math.round(rect.width); const height = Math.round(rect.height); const fullscreenWidth = Math.round(window.innerWidth); const fullscreenHeight = Math.round(window.innerHeight); const isFullscreenGeometry = Math.abs(width - fullscreenWidth) <= 1 && Math.abs(height - fullscreenHeight) <= 1 && Math.abs(rect.left) <= 1 && Math.abs(rect.top) <= 1; if (!ui.maximized && isFullscreenGeometry) { Store2.updateUi({ maximized: true, _normalWidth: ui.width, _normalHeight: ui.height, _normalX: ui.x, _normalY: ui.y }); updateWindowChrome(); return; } if (ui.maximized) { const userResized = !isFullscreenGeometry; if (!userResized) return; Store2.updateUi({ maximized: false, width, height, x: Math.max(0, Math.round(rect.left)), y: Math.max(0, Math.round(rect.top)) }); updateWindowChrome(); return; } Store2.updateUi({ width, height }); }) ); observer.observe(root); } function handleViewportResize() { const viewportChanged = window.innerWidth !== lastViewport.width || window.innerHeight !== lastViewport.height; lastViewport = { width: window.innerWidth, height: window.innerHeight }; if (!viewportChanged || Store2.getUi().minimized) return; if (Store2.getUi().maximized) { Store2.updateUi({ maximized: false }); } centerPanel(); updateWindowChrome(); } function init() { createAdoStyleEntry(); root = document.createElement("div"); root.id = "confluence-plus-root"; root.addEventListener("contextmenu", showContextMenu); document.body.appendChild(root); window.addEventListener("resize", handleViewportResize); render(); enableDrag(); observeResize(); unsubscribe = Store2.subscribe((type) => { if (type === "context") { contentFeatureUi = { spaceTreeOpen: false, aiLabelsOpen: false }; updatePageId(); } if (type === "ui" && Store2.getUi().minimized) { render(); } }); } return { init, render, toast, destroy() { unsubscribe?.(); hideContextMenu(); unbindThemeListener(); removeAdoStyleEntry(); window.removeEventListener("resize", handleViewportResize); root?.remove(); } }; })(); return UI2; } // src/plugins/confluence/runtime/core.js var images = { plugin: "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA2NCA2NCI+PGNpcmNsZSBjeD0iMzIiIGN5PSIzMiIgcj0iMjgiIGZpbGw9IiMwYzY2ZTQiLz48cmVjdCB4PSIxNCIgeT0iMjMiIHdpZHRoPSIzNiIgaGVpZ2h0PSIxOCIgcng9IjkiIGZpbGw9Im5vbmUiIHN0cm9rZT0iI2ZmZiIgc3Ryb2tlLXdpZHRoPSIzLjUiLz48Y2lyY2xlIGN4PSI0MSIgY3k9IjMyIiByPSI2IiBmaWxsPSIjZmZmIi8+PHBhdGggZD0iTTIwIDMyaDgiIHN0cm9rZT0iI2ZmZiIgc3Ryb2tlLXdpZHRoPSIzIiBzdHJva2UtbGluZWNhcD0icm91bmQiIG9wYWNpdHk9Ii43NSIvPjxwYXRoIGQ9Ik00MSAxOHYtNU00OSAyMWwzLTNNNTIgMzJoNCIgc3Ryb2tlPSIjZmZmIiBzdHJva2Utd2lkdGg9IjIuNSIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIi8+PC9zdmc+", configChevron: "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+PHBhdGggZD0iTTcgMTBsNSA1IDUtNSIgc3Ryb2tlPSJjdXJyZW50Q29sb3IiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBmaWxsPSJub25lIi8+PC9zdmc+" }; var windowIcons = { maximize: ``, restore: ``, close: `` }; var Settings = (() => { const validUiLanguages = ["system", "en", "zh-CN"]; const validThemes = ["system", "light", "dark"]; const defaults = { version: 1, general: { language: Config.settings.defaultLanguage, theme: Config.settings.defaultTheme, floatingButtonRight: Config.settings.defaultFloatingButtonRight }, modules: { content: {} } }; const cloneDefaults = () => JSON.parse(JSON.stringify(defaults)); function normalizeRight(value) { const number = Number(value); if (!Number.isFinite(number)) { return Config.settings.defaultFloatingButtonRight; } return Math.min( Config.settings.maxFloatingButtonRight, Math.max(Config.settings.minFloatingButtonRight, Math.floor(number)) ); } function normalize(source = {}) { const base = cloneDefaults(); const general = source.general || {}; base.general.language = validUiLanguages.includes(general.language) ? general.language : base.general.language; base.general.theme = validThemes.includes(general.theme) ? general.theme : base.general.theme; base.general.floatingButtonRight = normalizeRight( general.floatingButtonRight ); base.modules.content = { ...source.modules?.content || {} }; return base; } let current = normalize(Storage.loadSettings()); function save() { Storage.saveSettings(current); } return { validUiLanguages, validThemes, get() { return JSON.parse(JSON.stringify(current)); }, updateGeneral(key, value) { if (key === "language" && validUiLanguages.includes(value)) { current.general.language = value; } else if (key === "theme" && validThemes.includes(value)) { current.general.theme = value; } else if (key === "floatingButtonRight") { current.general.floatingButtonRight = normalizeRight(value); } else { return this.get(); } save(); return this.get(); }, resolveUiLanguage(value = current.general.language) { return value === "zh-CN" || value === "system" && /^zh(?:-|$)/i.test(navigator.language || "") ? "zh-CN" : "en"; }, reset() { current = cloneDefaults(); save(); } }; })(); var { getLocaleText } = createLocaleService({ getLanguage: () => Settings.get().general.language, localeMessages }); var AIConfig = createAIConfig({ Config, Storage, getLocaleText }); var Store = createStore({ Config, Storage, Utils }); var Router = createRouter({ Store, Logger, Utils }); var Http = createHttp({ Config, getLocaleText }); var AzureOpenAI = createAzureOpenAI({ Http, AIConfig, getLocaleText }); var ConfluenceApi = createConfluenceApi({ Config, Http }); var AILabelService = createAILabelService({ ConfluenceApi, AzureOpenAI }); var SpaceTreeService = createSpaceTreeService({ ConfluenceApi, Utils, Logger }); var ContentSessionCache = createContentSessionCache({ Logger }); var UI = createUI({ Config, Store, Router, Utils, Logger, Settings, AIConfig, AzureOpenAI, ConfluenceApi, AILabelService, SpaceTreeService, ContentSessionCache, getLocaleText, images, windowIcons }); function bootstrap() { Store.init(); Router.init(); UI.init(); Logger.info("initialized", { version: GM_info.script.version, origin: location.origin }); } // src/plugins/confluence/styles.js function installStyles({ config, addStyle } = {}) { const applyStyle = addStyle || (typeof GM_addStyle === "function" ? GM_addStyle : null); if (typeof applyStyle !== "function") { throw new Error("Confluence Plus requires GM_addStyle to install styles."); } applyStyle(` #confluence-plus-menu-overlay { position: fixed; inset: 0; z-index: 99998; background: rgba(9, 30, 66, .28); backdrop-filter: blur(1px); } .confluence-plus-floating-icon { position: fixed; right: 120px; top: 0; bottom: auto; z-index: 100004; display: inline-flex; } .confluence-plus-floating-icon-image { display: block; width: 48px; height: 48px; cursor: pointer; object-fit: contain; } .confluence-plus-tooltip-host { position: relative; } .confluence-plus-floating-icon.confluence-plus-tooltip-host { position: fixed; } .confluence-plus-tooltip { position: absolute; top: calc(100% + 8px); right: 0; z-index: 100005; padding: 8px 10px; border-radius: 5px; background: #172b4d; box-shadow: 0 4px 12px rgba(9, 30, 66, .25); color: #fff; font-size: 12px; line-height: 1.55; opacity: 0; pointer-events: none; transform: translateY(-4px); transition: opacity .15s ease, transform .15s ease; visibility: hidden; white-space: nowrap; } .confluence-plus-tooltip-host:hover .confluence-plus-tooltip, .confluence-plus-tooltip-host:focus-within .confluence-plus-tooltip { opacity: 1; transform: translateY(0); visibility: visible; } .confluence-plus-tooltip.confluence-plus-tooltip--right { bottom: auto; left: calc(100% + 10px); right: auto; top: 50%; transform: translateY(-50%) translateX(-4px); } .confluence-plus-tooltip.confluence-plus-tooltip--bottom { left: auto; right: 0; top: calc(100% + 10px); transform: translateY(-4px); } .confluence-plus-tooltip.confluence-plus-tooltip--left { left: auto; right: calc(100% + 10px); top: 50%; transform: translateY(-50%) translateX(4px); } .confluence-plus-tooltip.confluence-plus-tooltip--top { bottom: calc(100% + 10px); right: 0; top: auto; transform: translateY(4px); } .confluence-plus-tooltip-host:hover .confluence-plus-tooltip--right, .confluence-plus-tooltip-host:focus-within .confluence-plus-tooltip--right, .confluence-plus-tooltip-host:hover .confluence-plus-tooltip--left, .confluence-plus-tooltip-host:focus-within .confluence-plus-tooltip--left { transform: translateY(-50%) translateX(0); } .confluence-plus-tooltip-host:hover .confluence-plus-tooltip--bottom, .confluence-plus-tooltip-host:focus-within .confluence-plus-tooltip--bottom, .confluence-plus-tooltip-host:hover .confluence-plus-tooltip--top, .confluence-plus-tooltip-host:focus-within .confluence-plus-tooltip--top { transform: translateY(0); } .confluence-plus-ado-modal { align-items: center; display: flex; inset: 0; justify-content: center; position: fixed; z-index: 100003; } .confluence-plus-ado-modal[hidden] { display: none; } .confluence-plus-ado-modal-backdrop { background: rgba(9, 30, 66, .32); inset: 0; position: absolute; } .confluence-plus-ado-modal-panel { align-self: center; background: var(--confluence-plus-surface, #fff); border-radius: 9px; box-shadow: 0 18px 48px rgba(9, 30, 66, .22), 0 3px 12px rgba(9, 30, 66, .12); color: var(--confluence-plus-text, #172b4d); display: flex; flex: 0 1 auto; flex-direction: column; height: min(695px, calc(100dvh - 32px)); max-height: calc(100dvh - 32px); max-width: min(860px, calc(100vw - 36px)); overflow: hidden; position: relative; width: 860px; } .confluence-plus-ado-modal-header { align-items: center; border-bottom: 1px solid var(--confluence-plus-border, #dfe1e6); display: flex; flex: 0 0 auto; gap: 12px; padding: 18px 20px 16px; } .confluence-plus-ado-modal-mark { border-radius: 8px; display: block; flex: none; height: 36px; object-fit: cover; width: 36px; } .confluence-plus-ado-modal-header > div { min-width: 0; } .confluence-plus-ado-modal-header strong, .confluence-plus-ado-modal-header small { display: block; } .confluence-plus-ado-modal-header strong { font-size: 16px; line-height: 1.2; } .confluence-plus-ado-modal-header small { color: var(--confluence-plus-muted, #5e6c84); font-size: 12px; margin-top: 4px; } .confluence-plus-ado-modal-close { background: transparent; border: 0; border-radius: 5px; color: var(--confluence-plus-muted, #6b778c); cursor: pointer; font-size: 22px; height: 30px; margin-left: auto; width: 30px; } .confluence-plus-ado-modal-close:hover { background: var(--confluence-plus-hover, #ebecf0); color: var(--confluence-plus-text, #172b4d); } .confluence-plus-ado-modal-close:focus { outline: 0; } .confluence-plus-ado-modal-body { flex: 1 1 0; max-height: none; min-height: 0; overflow-y: auto; padding: 14px; scrollbar-color: var(--confluence-plus-border, #dfe1e6) transparent; scrollbar-width: thin; } .confluence-plus-ado-modal-body::-webkit-scrollbar { width: 8px; } .confluence-plus-ado-modal-body::-webkit-scrollbar-thumb { background: var(--confluence-plus-border, #dfe1e6); border-radius: 999px; } .confluence-plus-modal-config-list { display: grid; gap: 8px; min-height: 0; } .confluence-plus-modal-config-section { color: var(--confluence-plus-text, #172b4d); font-size: 12px; letter-spacing: .04em; margin: 4px 2px 0; text-transform: uppercase; } .confluence-plus-modal-config-group { border: 1px solid var(--confluence-plus-border, #dfe1e6); border-radius: 9px; overflow: hidden; } .confluence-plus-modal-config-group[hidden] { display: none !important; } .confluence-plus-modal-config-summary { align-items: center; background: var(--confluence-plus-control, #f8fafc); color: var(--confluence-plus-text, #172b4d); cursor: pointer; display: flex; font-size: 12px; font-weight: 700; justify-content: flex-start; letter-spacing: .04em; list-style: none; padding: 9px 36px 9px 12px; position: relative; text-transform: uppercase; } .confluence-plus-modal-config-summary::-webkit-details-marker { display: none; } .confluence-plus-modal-config-summary:hover { background: var(--confluence-plus-hover, #f1f5f9); } .confluence-plus-modal-config-chevron { align-items: center; color: var(--confluence-plus-muted, #5e6c84); display: inline-flex; height: 18px; justify-content: center; position: absolute; right: 12px; top: 50%; transform: translateY(-50%); transition: transform .18s ease; width: 18px; } .confluence-plus-modal-config-chevron img { display: block; height: 16px; width: 16px; } .confluence-plus-modal-config-group[open] .confluence-plus-modal-config-chevron { transform: translateY(-50%) rotate(180deg); } .confluence-plus-modal-config-group-content { display: grid; gap: 8px; padding: 8px; } .confluence-plus-modal-config-row { align-items: center; background: var(--confluence-plus-control, #f4f5f7); border: 1px solid var(--confluence-plus-border, #dfe1e6); border-radius: 9px; box-sizing: border-box; display: grid; gap: 14px; grid-template-columns: minmax(0, 1fr) minmax(150px, 42%); min-height: 60px; padding: 9px 12px; } .confluence-plus-modal-config-copy { min-width: 0; } .confluence-plus-modal-config-title, .confluence-plus-modal-config-description { display: block; } .confluence-plus-modal-config-title { color: var(--confluence-plus-text, #172b4d); font-size: 13px; font-weight: 650; } .confluence-plus-modal-config-description { color: var(--confluence-plus-muted, #5e6c84); font-size: 11px; line-height: 1.35; margin-top: 4px; } .confluence-plus-modal-config-control { min-width: 0; } .confluence-plus-modal-config-control input[type="number"], .confluence-plus-modal-config-control select { appearance: auto; background: var(--confluence-plus-surface, #fff); border: 2px solid var(--confluence-plus-border, #dfe1e6); border-radius: 7px; box-sizing: border-box; color: var(--confluence-plus-text, #172b4d); font: inherit; height: 40px; outline: 0; padding: 0 11px; transition: border-color .15s ease, box-shadow .15s ease, background-color .15s ease; width: 100%; } .confluence-plus-modal-config-control select { cursor: pointer; padding-right: 34px; } .confluence-plus-modal-config-control input:focus, .confluence-plus-modal-config-control select:focus { border-color: var(--confluence-plus-accent, #0c66e4); box-shadow: 0 0 0 2px color-mix(in srgb, var(--confluence-plus-accent, #0c66e4) 25%, transparent); } .confluence-plus-modal-config-control input[type="number"]::-webkit-inner-spin-button, .confluence-plus-modal-config-control input[type="number"]::-webkit-outer-spin-button { -webkit-appearance: none; margin: 0; } .confluence-plus-modal-config-control input[type="number"] { -moz-appearance: textfield; } .confluence-plus-modal-number-control { position: relative; } .confluence-plus-modal-number-control input { padding-right: 42px; } .confluence-plus-modal-number-control em { color: var(--confluence-plus-muted, #5e6c84); font-size: 12px; font-style: normal; position: absolute; right: 11px; top: 10px; } .confluence-plus-empty-settings { color: var(--confluence-plus-muted, #5e6c84); font-size: 12px; padding: 14px 8px; text-align: center; } .confluence-plus-ai-config { border: 1px solid var(--confluence-plus-border, #dfe1e6); border-radius: 10px; overflow: hidden; } .confluence-plus-ai-config-summary { align-items: center; background: var(--confluence-plus-control, #f8fafc); box-sizing: border-box; cursor: pointer; display: flex; gap: 14px; justify-content: space-between; list-style: none; min-height: 60px; padding: 9px 12px; } .confluence-plus-ai-config-summary::-webkit-details-marker { display: none; } .confluence-plus-ai-config-summary:hover { background: var(--confluence-plus-hover, #f1f5f9); } .confluence-plus-ai-config-copy { min-width: 0; } .confluence-plus-ai-config-summary-right { align-items: center; display: flex; flex: none; gap: 10px; } .confluence-plus-ai-status { align-items: center; border-radius: 999px; display: inline-flex; font-size: 11px; font-weight: 650; gap: 6px; padding: 6px 9px; white-space: nowrap; } .confluence-plus-ai-status::before { border-radius: 50%; content: ""; height: 7px; width: 7px; } .confluence-plus-ai-status.ai-status-unconfigured { background: #fff0b3; color: #974f0c; } .confluence-plus-ai-status.ai-status-unconfigured::before { background: #ff991f; } .confluence-plus-ai-status.ai-status-available { background: #e3fcef; color: #00875a; } .confluence-plus-ai-status.ai-status-available::before { background: #36b37e; } .confluence-plus-ai-chevron { align-items: center; display: inline-flex; height: 18px; justify-content: center; line-height: 1; transform: translateY(0); transition: transform .18s ease; width: 18px; } .confluence-plus-ai-chevron img { display: block; height: 16px; width: 16px; } .confluence-plus-ai-config[open] .confluence-plus-ai-chevron { transform: rotate(180deg); } .confluence-plus-ai-config-content { border-top: 1px solid var(--confluence-plus-border, #dfe1e6); max-width: 820px; padding: 18px 14px 14px; } .confluence-plus-ai-field { margin-bottom: 14px; } .confluence-plus-ai-field label { color: var(--confluence-plus-text, #172b4d); display: block; font-size: 12px; font-weight: 650; margin-bottom: 6px; } .confluence-plus-ai-field small { color: var(--confluence-plus-muted, #5e6c84); display: block; font-size: 11px; line-height: 1.35; margin: -2px 0 7px; } .confluence-plus-ai-field input, .confluence-plus-ai-field select { background: var(--confluence-plus-surface, #fff); border: 2px solid var(--confluence-plus-border, #dfe1e6); border-radius: 7px; box-sizing: border-box; color: var(--confluence-plus-text, #172b4d); font: inherit; font-size: 13px; height: 40px; outline: 0; padding: 0 11px; width: 100%; } .confluence-plus-ai-field input:focus, .confluence-plus-ai-field select:focus { border-color: var(--confluence-plus-accent, #0c66e4); box-shadow: 0 0 0 2px color-mix(in srgb, var(--confluence-plus-accent, #0c66e4) 25%, transparent); } .confluence-plus-ai-password-wrap { position: relative; } .confluence-plus-ai-password-wrap input { padding-right: 70px; } .confluence-plus-ai-show-key { background: transparent; border: 0; color: var(--confluence-plus-accent, #0c66e4); cursor: pointer; font-size: 11px; font-weight: 650; position: absolute; right: 10px; top: 11px; } .confluence-plus-ai-actions { align-items: center; display: flex; flex-wrap: nowrap; gap: 10px; justify-content: space-between; max-width: 100%; } .confluence-plus-ai-action-buttons { align-items: center; display: flex; flex: 0 0 auto; gap: 10px; margin-left: auto; } .confluence-plus-ai-actions button { border: 0; border-radius: 7px; cursor: pointer; flex: 0 0 auto; font: inherit; font-size: 12px; font-weight: 650; min-height: 38px; padding: 0 14px; white-space: nowrap; width: auto; } .confluence-plus-ai-primary { background: var(--confluence-plus-accent, #0c66e4); color: #fff; } .confluence-plus-ai-primary:hover { filter: brightness(.92); } .confluence-plus-ai-secondary { background: var(--confluence-plus-hover, #ebecf0); color: var(--confluence-plus-text, #172b4d); } .confluence-plus-ai-actions button:disabled { cursor: wait; opacity: .65; } .confluence-plus-ai-result { color: var(--confluence-plus-muted, #5e6c84); flex: 1 1 auto; font-size: 11px; line-height: 1.4; margin: 0; min-width: 0; overflow-wrap: anywhere; } .confluence-plus-ai-action-buttons .confluence-plus-ai-primary { min-width: 112px; } .confluence-plus-ai-tooltip-host { display: inline-flex; position: relative; } .confluence-plus-ai-tooltip { background: var(--confluence-plus-text, #172b4d); border: 1px solid color-mix(in srgb, var(--confluence-plus-surface, #fff) 18%, transparent); border-radius: 8px; bottom: calc(100% + 10px); box-shadow: 0 8px 22px rgba(0, 0, 0, .28); color: var(--confluence-plus-surface, #fff); font-size: 12px; font-weight: 500; left: 50%; line-height: 1.45; max-width: 280px; opacity: 0; padding: 9px 12px; pointer-events: none; position: absolute; text-align: left; transform: translate(-50%, 4px); transition: opacity .15s ease, transform .15s ease, visibility .15s ease; visibility: hidden; white-space: normal; width: max-content; z-index: 10004; } .confluence-plus-ai-tooltip::after { border: 5px solid transparent; border-top-color: var(--confluence-plus-text, #172b4d); content: ""; left: 50%; position: absolute; top: 100%; transform: translateX(-50%); } .confluence-plus-ai-tooltip-host:hover .confluence-plus-ai-tooltip, .confluence-plus-ai-tooltip-host:focus-within .confluence-plus-ai-tooltip { opacity: 1; transform: translate(-50%, 0); visibility: visible; } .confluence-plus-ai-result.ai-result-success { color: #00875a; } .confluence-plus-ai-result.ai-result-error { color: #de350b; } .confluence-plus-theme-dark .confluence-plus-ai-chevron img { filter: brightness(0) invert(1); } @media (max-width: 700px) { .confluence-plus-ai-actions { align-items: stretch; flex-wrap: wrap; } .confluence-plus-ai-action-buttons { margin-left: auto; } .confluence-plus-ai-result { flex-basis: 100%; margin-top: 2px; } } .confluence-plus-theme { --confluence-plus-bg: #fff; --confluence-plus-surface: #fff; --confluence-plus-text: #172b4d; --confluence-plus-muted: #5e6c84; --confluence-plus-border: #dfe1e6; --confluence-plus-control: #f4f5f7; --confluence-plus-accent: #0c66e4; --confluence-plus-hover: #ebecf0; --confluence-plus-backdrop: rgba(9, 30, 66, .32); color-scheme: light; } .confluence-plus-theme-dark { --confluence-plus-bg: #101214; --confluence-plus-surface: #1d2125; --confluence-plus-text: #e6edf3; --confluence-plus-muted: #a8b3c2; --confluence-plus-border: #3b434d; --confluence-plus-control: #252b33; --confluence-plus-accent: #579dff; --confluence-plus-hover: #30363d; --confluence-plus-backdrop: rgba(0, 0, 0, .58); color-scheme: dark; } .confluence-plus-theme .confluence-plus-ado-modal-backdrop { background: var(--confluence-plus-backdrop); } .confluence-plus-theme .confluence-plus-ado-modal-panel { background: var(--confluence-plus-surface); color: var(--confluence-plus-text); box-shadow: 0 18px 48px var(--confluence-plus-backdrop); } .confluence-plus-theme .confluence-plus-ado-modal-header { border-color: var(--confluence-plus-border); } .confluence-plus-theme .confluence-plus-ado-modal-header small, .confluence-plus-theme .confluence-plus-modal-config-description, .confluence-plus-theme .confluence-plus-modal-number-control em { color: var(--confluence-plus-muted); } .confluence-plus-theme .confluence-plus-modal-config-row { background: var(--confluence-plus-control); border-color: var(--confluence-plus-border); } .confluence-plus-theme .confluence-plus-modal-config-title, .confluence-plus-theme .confluence-plus-modal-config-summary, .confluence-plus-theme .confluence-plus-modal-config-section { color: var(--confluence-plus-text); } .confluence-plus-theme .confluence-plus-ado-modal-close { color: var(--confluence-plus-muted); } .confluence-plus-theme .confluence-plus-ado-modal-close:hover, .confluence-plus-theme .confluence-plus-modal-config-control input, .confluence-plus-theme .confluence-plus-modal-config-control select { background: var(--confluence-plus-surface); border-color: var(--confluence-plus-border); color: var(--confluence-plus-text); } .confluence-plus-theme .confluence-plus-ado-modal-close:hover { background: var(--confluence-plus-hover); } .confluence-plus-theme .confluence-plus-modal-config-control input:focus, .confluence-plus-theme .confluence-plus-modal-config-control select:focus { border-color: var(--confluence-plus-accent); box-shadow: 0 0 0 2px color-mix(in srgb, var(--confluence-plus-accent) 25%, transparent); } .confluence-plus-theme .confluence-plus-modal-config-chevron img { filter: none; } .confluence-plus-theme-dark .confluence-plus-modal-config-chevron img { filter: brightness(0) invert(1); } .confluence-plus-theme .confluence-plus-empty-settings { color: var(--confluence-plus-muted); } @media (max-width: 620px) { .confluence-plus-ado-modal-panel { height: calc(100dvh - 16px); max-width: calc(100vw - 16px); } .confluence-plus-modal-config-row { align-items: stretch; flex-direction: column; } .confluence-plus-modal-config-control { width: 100%; } .confluence-plus-modal-config-control select, .confluence-plus-modal-number-control input { width: 100%; } } #confluence-plus-root { position: fixed; z-index: 100000; font-size: 14px; color: var(--confluence-plus-text, #333); } #confluence-plus-root.confluence-plus-theme .confluence-plus-inline-feature-content, #confluence-plus-root.confluence-plus-theme .confluence-plus-modal-status, #confluence-plus-root.confluence-plus-theme .confluence-plus-title { color: var(--confluence-plus-text); } #confluence-plus-root.confluence-plus-theme .confluence-plus-modal-hint { color: var(--confluence-plus-muted); } #confluence-plus-root.confluence-plus-theme .confluence-plus-label-manual-input, #confluence-plus-root.confluence-plus-theme .confluence-plus-label-edit { background: var(--confluence-plus-surface); border-color: var(--confluence-plus-border); color: var(--confluence-plus-text); } #confluence-plus-root.confluence-plus-theme .confluence-plus-secondary-btn { background: var(--confluence-plus-surface); border-color: var(--confluence-plus-accent); color: var(--confluence-plus-accent); } #confluence-plus-root.confluence-plus-theme .confluence-plus-popconfirm { background: var(--confluence-plus-surface); border-color: var(--confluence-plus-border); color: var(--confluence-plus-text); } #confluence-plus-root.confluence-plus-theme .confluence-plus-popconfirm-message { color: var(--confluence-plus-muted); } #confluence-plus-root.confluence-plus-theme .confluence-plus-popconfirm::after { background: var(--confluence-plus-surface); border-color: var(--confluence-plus-border); } #confluence-plus-root.confluence-plus-theme .confluence-plus-confirm-cancel { background: var(--confluence-plus-hover); color: var(--confluence-plus-text); } .confluence-plus-context-menu { position: fixed; /* Keep the menu above the expanded root (z-index: 999999), matching the layering used by the pre-refactor UI. */ z-index: 1000003; width: 220px; box-sizing: border-box; border: 1px solid #dfe1e6; border-radius: 6px; background: white; box-shadow: 0 8px 24px rgba(9, 30, 66, .24); padding: 4px 0; color: #172b4d; font-size: 12px; } .confluence-plus-context-menu-item, .confluence-plus-context-menu-action { display: block; width: 100%; box-sizing: border-box; border: none; background: transparent; padding: 9px 12px; text-align: left; white-space: nowrap; user-select: none; } .confluence-plus-context-menu-action { color: #172b4d; cursor: pointer; font: inherit; } .confluence-plus-context-menu-action:hover { background: #deebff; color: #0747a6; } .confluence-plus-context-menu-info { color: #8993a4; cursor: default; } #confluence-plus-root.confluence-plus-content-window.confluence-plus-expanded { background: var(--confluence-plus-surface, #fff); border: 1px solid var(--confluence-plus-border, #dfe1e6); border-radius: 12px; box-shadow: 0 18px 48px rgba(9, 30, 66, .22); color: var(--confluence-plus-text, #172b4d); overflow: hidden; box-sizing: border-box; resize: both; min-width: min(720px, calc(100vw - 20px)); min-height: min(520px, calc(100vh - 20px)); max-width: 100vw; max-height: 100vh; } #confluence-plus-root.confluence-plus-content-window .confluence-plus-content-window-header { align-items: center; background: #172b4d; color: #fff; display: flex; flex: none; gap: 12px; height: 62px; min-height: 62px; padding: 0 16px; user-select: none; } #confluence-plus-root.confluence-plus-content-window .confluence-plus-content-window-mark { align-items: center; background: #0c66e4; border-radius: 8px; display: block; flex: none; height: 34px; object-fit: cover; width: 38px; } #confluence-plus-root.confluence-plus-content-window .confluence-plus-content-window-title-copy { min-width: 0; } #confluence-plus-root.confluence-plus-content-window .confluence-plus-content-window-title-copy strong, #confluence-plus-root.confluence-plus-content-window .confluence-plus-content-window-title-copy small { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } #confluence-plus-root.confluence-plus-content-window .confluence-plus-content-window-title-copy strong { font-size: 16px; line-height: 1.2; } #confluence-plus-root.confluence-plus-content-window .confluence-plus-content-window-title-copy small { color: #c7d3e3; font-size: 11px; margin-top: 2px; } #confluence-plus-root.confluence-plus-content-window .confluence-plus-content-window-spacer { flex: 1; } #confluence-plus-root.confluence-plus-content-window .confluence-plus-header-actions { flex: none; } #confluence-plus-root.confluence-plus-content-window .confluence-plus-content-window-action { background: transparent; border: 0; border-radius: 6px; color: #dbe7f5; cursor: pointer; display: inline-flex; align-items: center; justify-content: center; line-height: 1; padding: 0; font-size: 18px; height: 32px; width: 34px; } .confluence-plus-header-actions button svg { width: 18px; height: 18px; display: block; } #confluence-plus-root.confluence-plus-content-window .confluence-plus-content-window-action:hover { background: rgba(255, 255, 255, .14); color: #fff; } #confluence-plus-root.confluence-plus-content-window .confluence-plus-content-window-close { font-size: 24px; } #confluence-plus-root.confluence-plus-content-window .confluence-plus-content-window-body { display: flex; flex: 1; height: calc(100% - 62px); min-height: 0; position: relative; } #confluence-plus-root.confluence-plus-content-window .confluence-plus-content-window-scroll { display: flex; flex: 1; flex-direction: column; gap: 14px; min-height: 0; overflow: auto; padding: 16px 18px 22px; scrollbar-color: var(--confluence-plus-border, #dfe1e6) transparent; scrollbar-width: thin; } #confluence-plus-root.confluence-plus-content-window .confluence-plus-content-window-scroll::-webkit-scrollbar { width: 8px; } #confluence-plus-root.confluence-plus-content-window .confluence-plus-content-window-scroll::-webkit-scrollbar-thumb { background: var(--confluence-plus-border, #dfe1e6); border-radius: 999px; } .confluence-plus-content-feature-list { display: grid; gap: 12px; } .confluence-plus-content-feature { background: var(--confluence-plus-surface, #fff); border: 1px solid var(--confluence-plus-border, #dfe1e6); border-radius: 10px; overflow: hidden; } .confluence-plus-content-feature-summary { align-items: center; background: var(--confluence-plus-control, #f8fafc); cursor: pointer; display: flex; gap: 14px; justify-content: space-between; list-style: none; min-height: 60px; padding: 9px 12px; } .confluence-plus-content-feature-summary::-webkit-details-marker { display: none; } .confluence-plus-content-feature-summary:hover { background: var(--confluence-plus-hover, #f1f5f9); } .confluence-plus-content-feature-copy { min-width: 0; } .confluence-plus-content-feature-copy strong, .confluence-plus-content-feature-copy small { display: block; } .confluence-plus-content-feature-copy strong { color: var(--confluence-plus-text, #172b4d); font-size: 13px; font-weight: 650; } .confluence-plus-content-feature-copy small { color: var(--confluence-plus-muted, #5e6c84); font-size: 11px; line-height: 1.35; margin-top: 4px; } .confluence-plus-content-feature-ai-warning { color: #de350b; font-weight: 700; margin-right: 4px; } .confluence-plus-content-feature-chevron { align-items: center; display: inline-flex; flex: none; height: 20px; justify-content: center; transition: transform .18s ease; width: 20px; } .confluence-plus-content-feature-chevron img { display: block; height: 18px; width: 18px; } .confluence-plus-content-feature[open] .confluence-plus-content-feature-chevron { transform: rotate(180deg); } .confluence-plus-content-feature-body { border-top: 1px solid var(--confluence-plus-border, #dfe1e6); padding: 0; } .confluence-plus-feature-inline-host { min-width: 0; } .confluence-plus-inline-feature-content { box-sizing: border-box; color: var(--confluence-plus-text, #172b4d); min-width: 0; padding: 12px; } .confluence-plus-feature-inline-host .confluence-plus-space-tree { max-height: 360px; } .confluence-plus-inline-feature-error { color: #de350b; font-size: 12px; line-height: 1.45; padding: 12px; } #confluence-plus-root.confluence-plus-content-window .confluence-plus-content-feature-summary:focus-visible { outline: 2px solid var(--confluence-plus-accent, #0c66e4); outline-offset: 2px; } #confluence-plus-root.confluence-plus-content-window .confluence-plus-content-feature-chevron img { filter: none; } #confluence-plus-root.confluence-plus-content-window.confluence-plus-theme-dark .confluence-plus-content-feature-chevron img { filter: brightness(0) invert(1); } @media (max-width: 720px) { #confluence-plus-root.confluence-plus-content-window.confluence-plus-expanded { min-width: 0; width: calc(100vw - 16px) !important; } } #confluence-plus-root.confluence-plus-expanded { background: white; border: 1px solid #ddd; border-radius: 12px; box-shadow: 0 8px 24px rgba(0, 0, 0, .15); overflow: hidden; box-sizing: border-box; resize: both; min-width: ${config.ui.minWidth}px; min-height: ${config.ui.minHeight}px; max-width: 100vw; max-height: 100vh; } .confluence-plus-header { height: 48px; padding: 0 16px; background: #0052cc; color: white; display: flex; align-items: center; justify-content: space-between; cursor: move; user-select: none; } .confluence-plus-header-title { flex: 0 0 auto; min-width: 0; } .confluence-plus-session-label { margin-top: 2px; font-size: 11px; opacity: .8; } .confluence-plus-header-actions { display: flex; align-items: center; gap: 6px; } .confluence-plus-header button { width: 28px; height: 28px; border: none; border-radius: 6px; background: rgba(255, 255, 255, .2); color: white; font-size: 18px; line-height: 1; cursor: pointer; } .confluence-plus-header-icon-btn { font-size: 15px !important; } .confluence-plus-section { margin-bottom: 20px; } .confluence-plus-title { margin-bottom: 12px; font-weight: bold; } .confluence-plus-tree-toolbar { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-bottom: 10px; } .confluence-plus-tree-refresh { width: 28px; height: 28px; border: 1px solid var(--confluence-plus-border, #dfe1e6); border-radius: 6px; background: var(--confluence-plus-surface, #fff); color: var(--confluence-plus-text, #172b4d); cursor: pointer; font-size: 17px; } .confluence-plus-tree-refresh:hover { background: var(--confluence-plus-hover, #ebecf0); } .confluence-plus-space-tree { max-height: calc(100% - 48px); overflow: auto; padding: 4px 0; } .confluence-plus-tree-list { margin: 0; padding-left: 0; list-style: none; } .confluence-plus-tree-item .confluence-plus-tree-list { margin-left: 18px; } .confluence-plus-tree-row { position: relative; display: flex; align-items: center; gap: 6px; min-height: 32px; border-radius: 6px; padding: 3px 6px; } .confluence-plus-tree-row:hover { background: var(--confluence-plus-hover, #f4f5f7); } .confluence-plus-tree-current { background: color-mix(in srgb, var(--confluence-plus-accent, #0c66e4) 16%, var(--confluence-plus-surface, #fff)); color: var(--confluence-plus-accent, #0747a6); font-weight: 600; } .confluence-plus-tree-toggle, .confluence-plus-tree-toggle-spacer { width: 22px; height: 24px; flex: 0 0 22px; } .confluence-plus-tree-toggle { border: none; border-radius: 4px; background: transparent; color: var(--confluence-plus-muted, #42526e); cursor: pointer; font-size: 15px; line-height: 1; } .confluence-plus-tree-toggle:hover { background: var(--confluence-plus-hover, #dfe1e6); } .confluence-plus-tree-title-wrap { min-width: 0; flex: 1; display: flex; align-items: baseline; overflow: hidden; } .confluence-plus-tree-title { min-width: 0; flex: 0 1 auto; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .confluence-plus-tree-child-count { flex: 0 0 auto; margin-left: 4px; color: var(--confluence-plus-accent, #0052cc); font-size: 11px; font-weight: 600; white-space: nowrap; } .confluence-plus-tree-current-badge { flex: 0 0 auto; margin-left: 4px; border-radius: 999px; background: var(--confluence-plus-accent, #0052cc); color: var(--confluence-plus-surface, #fff); padding: 2px 6px; font-size: 10px; white-space: nowrap; } .confluence-plus-label-toolbar { align-items: end; background: var(--confluence-plus-control, #f7f9fb); border: 1px solid var(--confluence-plus-border, #dfe1e6); border-radius: 9px; display: grid; gap: 14px; grid-template-columns: minmax(0, 1fr) auto; padding: 10px 12px; } .confluence-plus-label-toolbar-copy { min-width: 0; } .confluence-plus-label-toolbar-copy .confluence-plus-title { color: var(--confluence-plus-text, #172b4d); margin: 0; } .confluence-plus-label-toolbar-copy .confluence-plus-modal-hint { margin-top: 4px; } .confluence-plus-label-generate-button, .confluence-plus-label-apply-button { align-self: end; background: var(--confluence-plus-accent, #0c66e4); border: 0; border-radius: 7px; color: #fff; cursor: pointer; font: inherit; font-size: 12px; font-weight: 650; height: 40px; padding: 0 14px; white-space: nowrap; } .confluence-plus-label-generate-button { min-width: 190px; } .confluence-plus-label-generate-button:hover:not(:disabled), .confluence-plus-label-apply-button:hover:not(:disabled) { filter: brightness(.92); } .confluence-plus-label-generate-button:disabled, .confluence-plus-label-apply-button:disabled { cursor: wait; opacity: .6; } .confluence-plus-label-results-header { color: var(--confluence-plus-text, #172b4d); font-size: 13px; font-weight: 700; margin: 14px 0 8px; max-width: 100%; } .confluence-plus-label-results-header-text { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .confluence-plus-label-title-tooltip { position: relative; } .confluence-plus-label-title-tooltip::after { background: var(--confluence-plus-text, #172b4d); border: 1px solid color-mix(in srgb, var(--confluence-plus-surface, #fff) 18%, transparent); border-radius: 7px; bottom: calc(100% + 8px); box-shadow: 0 8px 22px rgba(0, 0, 0, .28); color: var(--confluence-plus-surface, #fff); content: attr(data-confluence-plus-tooltip); font-size: 11px; font-weight: 500; left: 0; line-height: 1.4; max-width: min(420px, calc(100vw - 40px)); opacity: 0; padding: 8px 10px; pointer-events: none; position: absolute; transform: translateY(4px); transition: opacity .15s ease, transform .15s ease, visibility .15s ease; visibility: hidden; white-space: normal; width: max-content; z-index: 10004; } .confluence-plus-label-title-tooltip:hover::after, .confluence-plus-label-title-tooltip:focus-visible::after { opacity: 1; transform: translateY(0); visibility: visible; } .confluence-plus-label-list { display: grid; gap: 10px; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); margin-bottom: 12px; } .confluence-plus-label-item { align-items: center; background: var(--confluence-plus-control, #f7f9fb); border: 1px solid var(--confluence-plus-border, #dfe1e6); border-radius: 8px; display: grid; gap: 8px; grid-template-columns: 18px minmax(0, 1fr) 28px; min-height: 42px; padding: 7px 8px; } .confluence-plus-label-item input[type="checkbox"] { accent-color: var(--confluence-plus-accent, #0c66e4); height: 16px; width: 16px; } .confluence-plus-label-edit, .confluence-plus-label-manual-input { background: var(--confluence-plus-surface, #fff); border: 1px solid var(--confluence-plus-border, #dfe1e6); border-radius: 6px; box-sizing: border-box; color: var(--confluence-plus-text, #172b4d); font: inherit; height: 32px; min-width: 0; padding: 0 9px; width: 100%; } .confluence-plus-label-edit:focus, .confluence-plus-label-manual-input:focus { border-color: var(--confluence-plus-accent, #0c66e4); box-shadow: 0 0 0 2px color-mix(in srgb, var(--confluence-plus-accent, #0c66e4) 25%, transparent); outline: 0; } .confluence-plus-label-remove { width: 28px; height: 28px; border: none; border-radius: 6px; background: #ffebe6; color: #bf2600; cursor: pointer; font-size: 16px; } .confluence-plus-label-add-row { display: grid; gap: 8px; grid-template-columns: minmax(0, 1fr) auto; margin-bottom: 12px; } .confluence-plus-label-apply-row { display: flex; justify-content: flex-end; margin-top: 12px; } .confluence-plus-label-apply-tooltip-host { display: inline-flex; position: relative; } .confluence-plus-label-apply-tooltip-host::after { background: var(--confluence-plus-text, #172b4d); border: 1px solid color-mix(in srgb, var(--confluence-plus-surface, #fff) 18%, transparent); border-radius: 7px; bottom: calc(100% + 9px); color: #fff; content: attr(data-tooltip); font-size: 11px; line-height: 1.35; max-width: 260px; opacity: 0; padding: 7px 9px; pointer-events: none; position: absolute; right: 0; transform: translateY(4px); transition: opacity .15s ease, transform .15s ease; visibility: hidden; white-space: normal; width: max-content; z-index: 100006; } .confluence-plus-label-apply-tooltip-host:hover::after, .confluence-plus-label-apply-tooltip-host:focus-within::after, .confluence-plus-label-apply-tooltip-host:focus::after { opacity: 1; transform: translateY(0); visibility: visible; } #confluence-plus-root.confluence-plus-theme-dark .confluence-plus-label-apply-tooltip-host::after { background: #172b4d; border-color: #4c9aff; color: #fff; } @media (max-width: 700px) { .confluence-plus-label-toolbar { grid-template-columns: 1fr; } .confluence-plus-label-generate-button { width: 100%; } .confluence-plus-label-apply-row { justify-content: stretch; } .confluence-plus-label-apply-button { width: 100%; } } .confluence-plus-popconfirm-wrap { position: fixed; z-index: 2147483001; width: 300px; } .confluence-plus-popconfirm-wrap.is-dark .confluence-plus-popconfirm { background: #172b4d; border-color: #4c9aff; color: #fff; } .confluence-plus-popconfirm-wrap.is-dark .confluence-plus-popconfirm::after { background: #172b4d; border-color: #4c9aff; } .confluence-plus-popconfirm-wrap.is-dark .confluence-plus-popconfirm-title { color: #fff; } .confluence-plus-popconfirm-wrap.is-dark .confluence-plus-popconfirm-message { color: #c7d3e3; } .confluence-plus-popconfirm-wrap.is-dark .confluence-plus-confirm-cancel { background: #253858; color: #fff; } .confluence-plus-popconfirm-wrap.is-dark .confluence-plus-confirm-ok { background: #4c9aff; color: #172b4d; } .confluence-plus-popconfirm { position: relative; border: 1px solid #dfe1e6; border-radius: 8px; background: white; box-shadow: 0 8px 24px rgba(9, 30, 66, .22); padding: 14px; } .confluence-plus-popconfirm::after { content: ''; position: absolute; left: 24px; bottom: -7px; width: 12px; height: 12px; border-right: 1px solid #dfe1e6; border-bottom: 1px solid #dfe1e6; background: white; transform: rotate(45deg); } .confluence-plus-popconfirm-title { color: #172b4d; font-size: 14px; font-weight: 700; } .confluence-plus-popconfirm-message { margin-top: 7px; color: #42526e; font-size: 12px; line-height: 1.45; word-break: break-word; } .confluence-plus-popconfirm-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 12px; } .confluence-plus-popconfirm-actions button { border: none; border-radius: 6px; padding: 6px 10px; cursor: pointer; font-size: 12px; } .confluence-plus-confirm-cancel { background: #f1f2f4; color: #172b4d; } .confluence-plus-confirm-ok { background: #0052cc; color: white; } .confluence-plus-label-add-row .confluence-plus-secondary-btn { width: auto; } .confluence-plus-feature-btn { margin-bottom: 10px; } .confluence-plus-secondary-btn { width: 100%; border: 1px solid #0052cc; border-radius: 8px; background: white; color: #0052cc; padding: 9px; cursor: pointer; display: flex; align-items: center; justify-content: space-between; gap: 8px; text-align: left; } .confluence-plus-status-badge { display: inline-block; border-radius: 999px; padding: 3px 7px; font-size: 11px; font-weight: 600; white-space: nowrap; } .confluence-plus-status-badge.not-configured { background: #f1f2f4; color: #6b778c; } .confluence-plus-status-badge.unverified { background: #fff0b3; color: #7f5f01; } .confluence-plus-status-badge.available { background: #abf5d1; color: #006644; } .confluence-plus-status-badge.unavailable { background: #ffbdad; color: #bf2600; } .confluence-plus-modal-hint { margin-top: 6px; color: var(--confluence-plus-muted, #6b778c); font-size: 11px; line-height: 1.4; } .confluence-plus-modal-status { min-height: 18px; margin-top: 10px; font-size: 12px; white-space: pre-wrap; } .confluence-plus-modal-status.success { color: #16803c; } .confluence-plus-modal-status.error { color: #d93025; } .confluence-plus-toast { position: fixed; right: 20px; bottom: 20px; z-index: 100006; border-radius: 8px; background: #333; color: white; padding: 10px 14px; } .confluence-plus-toast.error { background: #d93025; } .confluence-plus-toast-stack { display: flex; flex-direction: column; gap: 10px; max-width: min(430px, calc(100vw - 32px)); pointer-events: none; position: fixed; right: 20px; top: 20px; width: min(430px, calc(100vw - 32px)); z-index: 100006; } .confluence-plus-toast-stack[hidden] { display: none; } .confluence-plus-toast-stack .confluence-plus-toast { align-items: flex-start; background: #fff; border: 1px solid #b36b00; border-radius: 10px; bottom: auto; box-shadow: 0 16px 42px rgba(9,30,66,.28); color: #172b4d; display: flex; gap: 12px; max-width: none; opacity: 0; padding: 14px 16px 30px; pointer-events: auto; position: relative; right: auto; top: auto; transform: translateX(18px); transition: opacity .2s ease, transform .2s ease; visibility: hidden; width: 100%; } .confluence-plus-toast-stack .confluence-plus-toast[hidden] { display: none; } .confluence-plus-toast-stack .confluence-plus-toast.is-visible { opacity: 1; transform: translateX(0); visibility: visible; } .confluence-plus-toast-stack .confluence-plus-toast.is-leaving { opacity: 0; transform: translateX(18px); } .confluence-plus-toast-icon { align-items: center; background: #b36b00; border-radius: 50%; color: #fff; display: inline-flex; flex: 0 0 24px; font-size: 14px; font-weight: 800; height: 24px; justify-content: center; line-height: 1; width: 24px; } .confluence-plus-toast-copy { display: flex; flex: 1; flex-direction: column; gap: 2px; min-width: 0; } .confluence-plus-toast-copy strong { font-size: 13px; } .confluence-plus-toast-copy span, .confluence-plus-toast-close { color: #5e6c84; } .confluence-plus-toast-copy span { font-size: 12px; line-height: 1.45; overflow-wrap: anywhere; } .confluence-plus-toast-copy small { bottom: 8px; color: #5e6c84; font-size: 10px; line-height: 1; position: absolute; right: 12px; } .confluence-plus-toast-close { background: transparent; border: 0; border-radius: 5px; cursor: pointer; flex: 0 0 auto; font-size: 20px; height: 24px; line-height: 1; padding: 0 4px; } .confluence-plus-toast-close:hover { background: #ebecf0; color: #172b4d; } .confluence-plus-toast-close:focus-visible { outline: 2px solid #0c66e4; outline-offset: 2px; } .confluence-plus-toast-stack.is-dark .confluence-plus-toast { background: #172b4d; border-color: #4c9aff; color: #fff; } .confluence-plus-toast-stack.is-dark .confluence-plus-toast-copy span, .confluence-plus-toast-stack.is-dark .confluence-plus-toast-close, .confluence-plus-toast-stack.is-dark .confluence-plus-toast-copy small { color: #c7d3e3; } `); } // src/plugins/confluence/entry.js installStyles({ config: Config }); installSharedTooltips(); bootstrap(); })();