// ==UserScript== // @name Confluence Plus // @namespace https://docs.scriptcat.org/ // @version 1.0.0 // @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/* // @connect * // @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 // @run-at document-idle // @license MIT // ==/UserScript== (function () { 'use strict'; /******************************************************** * CONFIG ********************************************************/ const images = { plugin: 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA2NCA2NCI+PGNpcmNsZSBjeD0iMzIiIGN5PSIzMiIgcj0iMjgiIGZpbGw9IiMwYzY2ZTQiLz48cGF0aCBkPSJtMjAgNDQgMjEtMjEiIHN0cm9rZT0iI2ZmZiIgc3Ryb2tlLXdpZHRoPSI2IiBzdHJva2UtbGluZWNhcD0icm91bmQiLz48cGF0aCBkPSJNMTggMThsMiA1IDUgMi01IDItMiA1LTItNS01LTIgNS0yWk00NSAzOWwxLjUgMy41TDUwIDQ0bC0zLjUgMS41TDQ1IDQ5bC0xLjUtMy41TDQwIDQ0bDMuNS0xLjVaIiBmaWxsPSIjZmZmIi8+PHBhdGggZD0iTTM2IDE4aDh2OCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjZmZmIiBzdHJva2Utd2lkdGg9IjQiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIvPjwvc3ZnPg==', configChevron: 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+PHBhdGggZD0iTTcgMTBsNSA1IDUtNSIgc3Ryb2tlPSJjdXJyZW50Q29sb3IiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBmaWxsPSJub25lIi8+PC9zdmc+', }; const windowIcons = { maximize: ``, restore: ``, close: ``, }; const Config = { storageKey: 'confluence_plus_state', api: { v1Path: '/wiki/rest/api', v2Path: '/wiki/api/v2', }, request: { timeout: 15000, }, 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: 1000, }, }; /******************************************************** * UTILS ********************************************************/ const 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 { // Fall back to the metadata lookup below. } // Confluence also exposes the current content id in page metadata on // some routes where the URL itself does not contain /pages/{id}. 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); }; }, }; /******************************************************** * LOGGER ********************************************************/ const 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); }, }; /******************************************************** * STORAGE ********************************************************/ const Storage = { loadData() { try { const raw = GM_getValue(Config.storageKey, '{}'); const data = JSON.parse(raw); // Remove credentials written by pre-session versions. 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 || {}; // Only the panel size is persistent. Menu state and position // intentionally reset for every page visit. 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)); }, }; /******************************************************** * SETTINGS ********************************************************/ const Settings = (() => { const validLanguages = ['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 = validLanguages.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 { validLanguages, validThemes, get() { return JSON.parse(JSON.stringify(current)); }, updateGeneral(key, value) { if (key === 'language' && validLanguages.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(); }, resolveLanguage(value = current.general.language) { return value === 'zh-CN' || (value === 'system' && /^zh(?:-|$)/i.test(navigator.language || '')) ? 'zh-CN' : 'en'; }, reset() { current = cloneDefaults(); save(); }, }; })(); const 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: '中文', 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…', 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…', 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?', cancel: 'Cancel', apply: 'Apply', selectNewValidLabel: 'Select at least one new valid label', applyingLabels: 'Applying labels…', addedLabelsRefreshing: 'Added {{count}} label(s). Refreshing page…', loadingAndGenerating: 'Loading page content and generating suggestions…', 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 设置', configure: '配置 Confluence Plus', close: '关闭', general: '常规', contentModule: '内容', content: '内容', language: '语言', languageDescription: '选择插件显示语言。', theme: '主题', themeDescription: '浅色、深色或跟随系统。', floatingPosition: '浮动按钮位置', floatingPositionDescription: '距离屏幕右侧的距离。', system: '系统', english: 'English', chinese: '中文', light: '浅色', dark: '深色', pixels: 'px', noSettings: '暂时没有可配置项。', aiConfiguration: 'AI 配置', aiConfigurationDescription: '配置 Azure OpenAI,以供 Confluence 内容功能调用。', aiStatusNotConfigured: '未配置', aiStatusConfigured: '已配置', aiStatusAvailable: '已配置', aiStatusUnavailable: '不可用', apiEndpoint: 'API 端点', apiEndpointDescription: '例如:https://your-resource.openai.azure.com/', apiKey: 'API Key', apiVersion: 'API 版本', apiVersionDescription: '用于 Chat Completions 的 Azure OpenAI API 版本。', apiKeyPlaceholder: '输入 Azure OpenAI API Key', show: '显示', hide: '隐藏', modelDeployment: '部署', modelDeploymentDescription: '输入 Azure OpenAI 使用的部署名称。', modelDeploymentPlaceholder: '请输入部署名称', tokenParameter: 'Token 数量参数', tokenParameterDescription: '较新的推理模型使用 max_completion_tokens,旧模型使用 max_tokens。', maxCompletionTokens: 'max_completion_tokens', maxTokens: 'max_tokens', notTested: '尚未测试连接。', connecting: '正在使用指定的部署连接 Azure OpenAI…', testConnection: '测试连接', testConnectionTooltip: '使用指定的部署测试连接。', clear: '清除', missingEndpointOrKey: 'Endpoint 和 API Key 不能为空。', deploymentRequired: '请先输入部署名称。', deploymentVerificationFailed: '部署验证返回了无效响应。', deploymentVerified: '部署连接成功。', contentToolTitle: '内容管理工具', contentToolSubtitle: '使用工具管理内容', spacePageTree: 'Space 页面树', spacePageTreeDescription: '浏览并展开当前 Space 的完整页面树。', generateLabel: '生成 Label', generateLabelDescription: '使用已配置的 AI 分析当前页面并生成标签建议。', aiConfigurationRequired: '需要 AI 配置:', currentSpaceIcon: '当前 Space 图标', leftClick: '左键:打开功能窗口', rightClick: '右键:打开配置窗口', maximizeWindow: '最大化窗口', restoreWindow: '还原窗口', closeWindow: '关闭窗口', openPage: '打开页面', loadingSpaceTree: '正在加载 Space 页面树…', reloadSpaceTree: '重新加载 Space 页面树', collapseChildren: '收起子页面', expandChildren: '展开子页面', childPages: '{{count}} 个子页面', currentPageBadge: '当前页面', noPagesInSpace: '当前 Space 中没有找到页面。', loadedPages: '已加载 {{count}} 个页面', loadedPagesFromCache: '已从会话缓存加载 {{count}} 个页面', generateSuggestionsForPage: '为当前页面生成建议', pageContentSentToAi: '页面标题和内容将发送到已配置的 Azure OpenAI 端点。', generateLabelSuggestions: '分析当前页面并生成标签建议', generateSuggestions: '生成建议', recommendedLabelsFor: '{{title}} 的推荐标签', recommendedLabels: '推荐标签', addLabelManually: '手动添加标签', add: '添加', applySelectedLabels: '应用选中的标签', applySelectedLabelsTitle: '将选中的标签添加到当前 Confluence 页面', removeLabel: '移除标签', applyLabelsQuestion: '应用标签?', cancel: '取消', apply: '应用', selectNewValidLabel: '至少选择一个有效的新标签', applyingLabels: '正在应用标签…', addedLabelsRefreshing: '已添加 {{count}} 个标签,正在刷新页面…', loadingAndGenerating: '正在加载页面内容并生成建议…', configureAiBeforeLabels: '生成标签前请先配置 AI', generatedSuggestions: '已生成 {{count}} 条建议', noNewLabelSuggestions: '未生成新的标签建议', labelsCount: '{{count}} 个 Label:{{labels}}', aiConfigurationRequiredError: '请先配置并保存可用的 AI 配置。', aiLabelResponseMissing: 'AI 未返回可识别的 Label 建议。', aiLabelResponseInvalid: 'AI 返回的 Label 建议不是有效 JSON。', invalidPageId: '当前页面没有有效的 Confluence Page ID。', missingSpaceId: '当前页面没有可识别的 Space ID。', missingSpaceInfo: '当前 Space 没有可识别的名称或 Space Key。', endpointHttpsRequired: 'Azure OpenAI 端点必须使用 HTTPS。', unsupportedV1Endpoint: '暂不支持 Azure OpenAI /openai/v1 端点,请使用 Azure resource endpoint。', endpointMustNotContainCredentials: 'Endpoint 不应包含账号、查询参数或 hash。', endpointRequired: '请输入 Azure OpenAI Endpoint。', apiKeyRequired: '请输入 Azure OpenAI API Key。', apiVersionRequired: 'API Version 不能为空。', azureNonJsonResponse: 'Azure OpenAI 返回了非 JSON 响应。', azureCrossOriginRequestFailed: 'Azure OpenAI 跨域请求失败。', azureApiTimedOut: 'Azure OpenAI API 请求超时。', gmRequestUnavailable: 'UserScript 跨域请求接口不可用,请确认脚本已授予 GM_xmlhttpRequest 权限。', confluenceNonJsonResponse: 'Confluence 返回了非 JSON 响应,可能需要重新登录。', confluenceApiTimedOut: 'Confluence API 请求超时。', }, }; const getLocaleText = (key, variables = {}) => { const language = Settings.resolveLanguage(); const text = localeMessages[language]?.[key] || key; return text.replace(/\{\{(\w+)\}\}/g, (_, name) => variables[name] ?? ''); }; /******************************************************** * AI CONFIGURATION ********************************************************/ const AIConfig = (() => { const defaults = { schemaVersion: 2, endpoint: '', apiKey: '', apiVersion: Config.ai.defaultApiVersion, modelsApiVersion: Config.ai.defaultModelsApiVersion, deployment: '', deployments: [], tokenParameter: 'max_completion_tokens', lastCheck: null, }; // Start with the current schema intentionally; no legacy AI configuration is migrated. const stored = Storage.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) { Storage.saveAi(config); } function normalizeEndpoint(endpoint) { const value = endpoint.trim().replace(/\/+$/, ''); const url = new URL(value); if (url.protocol !== 'https:') { throw new Error(getLocaleText('endpointHttpsRequired')); } if (/\/openai\/v1\/?$/i.test(url.pathname)) { throw new Error(getLocaleText('unsupportedV1Endpoint')); } if (url.username || url.password || url.search || url.hash) { throw new Error(getLocaleText('endpointMustNotContainCredentials')); } return url.origin + url.pathname.replace(/\/+$/, ''); } function validate(candidate) { const next = { ...config, ...candidate, }; if (!next.endpoint) { throw new Error(getLocaleText('endpointRequired')); } if (!next.apiKey) { throw new Error(getLocaleText('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(getLocaleText('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, }; Storage.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, }; Storage.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, }, }; Storage.saveAi(config); }, getStatus() { if (!config.endpoint || !config.apiKey || !config.deployment) { return { key: 'not-configured', label: getLocaleText('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: getLocaleText('aiStatusConfigured'), }; } return config.lastCheck.ok ? { key: 'available', label: getLocaleText('aiStatusConfigured'), } : { key: 'unavailable', label: getLocaleText('aiStatusUnavailable'), }; }, clear() { config = { ...defaults }; Storage.clearAi(); }, isConfigured() { return Boolean(config.endpoint && config.apiKey); }, }; })(); /******************************************************** * STORE ********************************************************/ const Store = (() => { const state = { context: { pageId: '', url: location.href, }, ui: { x: 120, y: 120, width: Config.ui.defaultWidth, height: Config.ui.defaultHeight, minimized: true, maximized: false, }, }; const listeners = []; const persistUi = Utils.debounce(() => { Storage.saveUi(state.ui); }); function notify(type, value) { listeners.forEach((listener) => { listener(type, value, state); }); } return { init() { Object.assign(state.ui, Storage.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); } }; }, }; })(); /******************************************************** * ROUTER / PAGE CONTEXT ********************************************************/ const Router = (() => { let lastUrl = ''; function getContext() { const url = location.href; return { url, pageId: Utils.extractPageId(url), }; } function emitIfChanged() { const context = getContext(); if (context.url === lastUrl) { return; } lastUrl = context.url; // Reset before publishing the new route so the panel cannot keep // displaying tree data loaded for the previous Space. Store.resetPanelSession(); Store.setContext(context); Logger.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, }; })(); /******************************************************** * HTTP CLIENT ********************************************************/ const Http = { 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: Config.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( getLocaleText('azureNonJsonResponse') )); } }, onerror() { reject(new Error(getLocaleText('azureCrossOriginRequestFailed'))); }, ontimeout() { reject(new Error(getLocaleText('azureApiTimedOut'))); }, }); }); }, async request(url, options = {}) { if (options.crossOrigin) { if (typeof GM_xmlhttpRequest !== 'function') { throw new Error(getLocaleText('gmRequestUnavailable')); } // Never fall back to window.fetch for Azure OpenAI requests: that would // reintroduce the browser CORS restriction this manager bypasses. return this.requestViaManager(url, options); } const controller = new AbortController(); const timeoutId = setTimeout(() => { controller.abort(); }, Config.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( getLocaleText('confluenceNonJsonResponse') ); } return await response.json(); } catch (error) { if (error.name === 'AbortError') { throw new Error(getLocaleText('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 || {}), }, }); }, }; /******************************************************** * AZURE OPENAI API SERVICES ********************************************************/ const AzureOpenAI = { 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 Http.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(getLocaleText('deploymentVerificationFailed')); } return response; }, async testConnection(candidate) { const config = AIConfig.validate(candidate); if (!config.deployment) { throw new Error(getLocaleText('deploymentRequired')); } await this.verifyDeployment(config); return { deployments: [{ id: config.deployment, label: config.deployment }], deploymentCount: 1, message: getLocaleText('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 = AIConfig.get(); if (!AIConfig.isConfigured() || !config.deployment) { throw new Error(getLocaleText('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, 24000), ].join('\n\n'); const response = await Http.post( this.getChatCompletionsUrl(config), { messages: [ { role: 'user', content: prompt, }, ], response_format: { type: 'json_object' }, [config.tokenParameter]: 6000, }, { 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(getLocaleText('aiLabelResponseMissing')); } let parsed; try { parsed = JSON.parse(match[0]); } catch { throw new Error(getLocaleText('aiLabelResponseInvalid')); } return Array.isArray(parsed.labels) ? parsed.labels : []; }, }; /******************************************************** * CONFLUENCE API SERVICES ********************************************************/ const ConfluenceApi = { getV1Url(path) { return `${location.origin}${Config.api.v1Path}${path}`; }, getV2Url(path) { return `${location.origin}${Config.api.v2Path}${path}`; }, folders: { get(folderId) { return Http.get( ConfluenceApi.getV2Url( `/folders/${encodeURIComponent(folderId)}` ) ); }, }, spaces: { get(spaceId) { return Http.get( ConfluenceApi.getV2Url( `/spaces/${encodeURIComponent(spaceId)}` ) ); }, async getByKey(spaceKey) { const query = new URLSearchParams({ keys: spaceKey, limit: '1', }); const data = await Http.get( `${ConfluenceApi.getV2Url('/spaces')}?${query}` ); return Array.isArray(data.results) ? data.results[0] : null; }, }, pages: { get(pageId) { return Http.get( ConfluenceApi.getV2Url(`/pages/${pageId}`) ); }, getForAi(pageId) { const query = new URLSearchParams({ expand: 'body.storage,metadata.labels', }); return Http.get( `${ConfluenceApi.getV1Url(`/content/${pageId}`)}?${query}` ); }, addLabels(pageId, labels) { return Http.post( ConfluenceApi.getV1Url(`/content/${pageId}/label`), labels.map((name) => ({ prefix: 'global', name, })) ); }, getChildren(pageId) { return Http.get( ConfluenceApi.getV2Url( `/pages/${pageId}/children` ) ); }, getSpacePages(spaceId, cursor = '') { const query = new URLSearchParams({ limit: '250', }); if (cursor) { query.set('cursor', cursor); } return Http.get( `${ConfluenceApi.getV2Url( `/spaces/${encodeURIComponent(spaceId)}/pages` )}?${query}` ); }, }, pageStates: { get(pageId) { return Http.get( ConfluenceApi.getV1Url( `/content/${pageId}/state` ) ); }, }, }; /******************************************************** * AI LABEL SERVICE ********************************************************/ const AILabelService = { 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 ConfluenceApi.pages.getForAi(pageId); const existingLabels = this.getExistingLabels(page); const body = page.body?.storage?.value || ''; const suggestions = await AzureOpenAI.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), }; }, }; /******************************************************** * SPACE TREE SERVICE ********************************************************/ const SpaceTreeService = { 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 = Utils.extractPageId(link.href); return id && link.getClientRects().length; }); const containsCurrent = pageLinks.some( (link) => Utils.extractPageId(link.href) === String(currentPageId) ); const score = pageLinks.length + (containsCurrent ? 1000 : 0); if (score > bestScore) { best = { container, links: pageLinks, }; bestScore = score; } }); if (!best || !best.links.length) { return []; } const items = []; const seen = new Set(); const stack = []; best.links.forEach((link) => { const id = Utils.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 ConfluenceApi.pages.getSpacePages( spaceId, cursor ); const results = Array.isArray(data.results) ? data.results : []; pages.push(...results); cursor = this.getNextCursor(data); } while (cursor); // The pages endpoint does not include folder entities. Pages whose // parentType is folder still expose the folder id, so load those // folder records separately and merge them into the tree. const folders = 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 ConfluenceApi.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) { Logger.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 = 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 = 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 = 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, }; }, }; /******************************************************** * CONTENT SESSION CACHE ********************************************************/ const ContentSessionCache = (() => { 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) { Logger.warn('session-cache.read-failed', { name, error }); return null; } } function write(name, value) { try { sessionStorage.setItem(`${prefix}${name}`, JSON.stringify(value)); } catch (error) { Logger.warn('session-cache.write-failed', { name, error }); } } function remove(name) { try { sessionStorage.removeItem(`${prefix}${name}`); } catch (error) { Logger.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)); }, }; })(); /******************************************************** * UI PANEL ********************************************************/ const UI = (() => { let root; let unsubscribe; let overlay; let contextMenu; let adoStyleFab; let adoStyleConfig; let themeMediaQuery; let themeChangeHandler; let contentFeatureUi = { spaceTreeOpen: false, aiLabelsOpen: false, }; let lastViewport = { width: window.innerWidth, height: window.innerHeight, }; function applyLayout() { if (!root) return; const ui = Store.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) { // Use an explicit border-box size so the resize observer can tell // the true fullscreen geometry from a user resize. 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 = Store.getUi(); const width = Math.min( ui.width, Math.max(Config.ui.minWidth, window.innerWidth - 24) ); const height = Math.min( ui.height, Math.max(Config.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)); Store.updateUi({ x, y, width, height, }); } function setMinimized(minimized) { Store.updateUi({ minimized }); render(); } function updateWindowChrome() { if (!root) return; applyLayout(); const button = root.querySelector('#confluence-plus-toggle-maximize'); if (!button) return; const maximized = Store.getUi().maximized; button.innerHTML = maximized ? windowIcons.restore : windowIcons.maximize; button.title = getLocaleText(maximized ? 'restoreWindow' : 'maximizeWindow'); button.setAttribute( 'aria-label', getLocaleText(maximized ? 'restoreWindow' : 'maximizeWindow') ); } function toggleMaximized() { const ui = Store.getUi(); if (ui.maximized) { Store.updateUi({ maximized: false, width: ui._normalWidth || ui.width, height: ui._normalHeight || ui.height, x: ui._normalX ?? ui.x, y: ui._normalY ?? ui.y, }); } else { Store.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 images.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 = images.plugin; icon.src = getCurrentSpaceIconUrl() || fallback; icon.alt = getLocaleText('currentSpaceIcon'); icon.onerror = () => { if (icon.src !== fallback) icon.src = fallback; }; } function renderFloatingButtonLocale() { const tooltip = adoStyleFab?.querySelector('.confluence-plus-tooltip'); if (!tooltip) return; tooltip.innerHTML = `${getLocaleText('leftClick')}
${getLocaleText('rightClick')}`; } function applyFloatingButtonPosition() { if (!adoStyleFab) return; adoStyleFab.style.right = `${Settings.get().general.floatingButtonRight}px`; renderFloatingButtonLocale(); } function applyConfigTheme() { const settings = Settings.get(); const isDark = settings.general.theme === 'dark' || (settings.general.theme === 'system' && window.matchMedia?.('(prefers-color-scheme: dark)').matches); 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 (Settings.get().general.theme === 'system') { applyConfigTheme(); } }; if (themeMediaQuery.addEventListener) { themeMediaQuery.addEventListener('change', themeChangeHandler); } else { themeMediaQuery.addListener(themeChangeHandler); } } function unbindThemeListener() { if (!themeMediaQuery || !themeChangeHandler) return; if (themeMediaQuery.removeEventListener) { themeMediaQuery.removeEventListener('change', themeChangeHandler); } else { themeMediaQuery.removeListener(themeChangeHandler); } themeMediaQuery = null; themeChangeHandler = null; } function closeAdoStyleConfig() { if (!adoStyleConfig) return; adoStyleConfig.hidden = true; adoStyleConfig.setAttribute('aria-hidden', 'true'); } function getContentAiSummaryStatus() { const config = AIConfig.get(); const configured = Boolean( config.endpoint.trim() && config.apiKey.trim() && config.deployment.trim() ); return { key: configured ? 'ai-status-available' : 'ai-status-unconfigured', label: configured ? getLocaleText('aiStatusAvailable') : getLocaleText('aiStatusNotConfigured'), }; } function renderContentAiDeployments() { const options = adoStyleConfig?.querySelector('#confluencePlusContentAiDeploymentOptions'); if (!options) return; options.innerHTML = ''; AIConfig.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 getLocaleText('notTested'); if (check.status === 'success' && Number.isFinite(check.deploymentCount)) { return getLocaleText('deploymentVerified'); } return check.message || getLocaleText('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 = AIConfig.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 = () => { AIConfig.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 = getLocaleText(visible ? 'show' : 'hide'); }); clearButton.addEventListener('click', () => { AIConfig.clear(); endpoint.value = ''; apiKey.value = ''; apiVersion.value = '2025-03-01-preview'; deployment.value = ''; tokenParameter.value = 'max_completion_tokens'; renderContentAiDeployments(); setResult(getLocaleText('notTested')); refreshContentAiSummary(); }); testButton.addEventListener('click', async () => { updateDraft(); const draft = AIConfig.get(); if (!draft.endpoint || !draft.apiKey) { setResult(getLocaleText('missingEndpointOrKey'), 'error'); return; } testButton.disabled = true; setResult(getLocaleText('connecting')); try { const candidate = AIConfig.validate(draft); const connection = await AzureOpenAI.testConnection(candidate); const selectedDeployment = deployment.value.trim() || connection.deployments[0]?.id || ''; AIConfig.updateDraft({ ...candidate, deployment: selectedDeployment, deployments: connection.deployments, }); AIConfig.recordHealth(true, connection.message, { deploymentCount: connection.deploymentCount, v1: AzureOpenAI.isV1Endpoint(candidate.endpoint), }); deployment.value = selectedDeployment; renderContentAiDeployments(); setResult(connection.message, 'success'); refreshContentAiSummary(); } catch (error) { AIConfig.recordHealth(false, error.message); setResult(error.message, 'error'); refreshContentAiSummary(); } finally { testButton.disabled = false; } }); } function applyConfigLocale() { 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, getLocaleText(key)); } else { element.textContent = getLocaleText(key); } }); const title = adoStyleConfig.querySelector('#confluencePlusConfigTitle'); if (title) { title.textContent = `${getLocaleText('settings')} (v${GM_info.script.version})`; } adoStyleConfig.querySelectorAll('[data-confluence-plus-option-locale]').forEach((option) => { option.textContent = getLocaleText(option.dataset.confluencePlusOptionLocale); }); const language = adoStyleConfig.querySelector('#confluencePlusConfigLanguage'); const theme = adoStyleConfig.querySelector('#confluencePlusConfigTheme'); if (language) language.setAttribute('aria-label', getLocaleText('language')); if (theme) theme.setAttribute('aria-label', getLocaleText('theme')); const showKey = adoStyleConfig.querySelector('[data-content-ai-show-key]'); if (showKey) { const keyInput = adoStyleConfig.querySelector('#confluencePlusContentAiKey'); showKey.textContent = getLocaleText(keyInput?.type === 'text' ? 'hide' : 'show'); } const current = AIConfig.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 = Settings.get().general.language; const theme = Settings.get().general.theme; const right = Settings.get().general.floatingButtonRight; const aiConfig = AIConfig.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) => { Settings.updateGeneral('language', event.currentTarget.value); applyConfigLocale(); applyConfigTheme(); renderFloatingButtonLocale(); if (!Store.getUi().minimized) { render(true); } console.log( '[Confluence Plus] Configuration dialog language-updated HTML:\n' + adoStyleConfig.outerHTML ); } ); adoStyleConfig.querySelector('#confluencePlusConfigTheme')?.addEventListener( 'change', (event) => { Settings.updateGeneral('theme', event.currentTarget.value); applyConfigLocale(); applyConfigTheme(); console.log( '[Confluence Plus] Configuration dialog theme-updated HTML:\n' + adoStyleConfig.outerHTML ); } ); adoStyleConfig.querySelector('#confluencePlusConfigFloatingRight')?.addEventListener( 'change', (event) => { Settings.updateGeneral('floatingButtonRight', event.currentTarget.value); applyFloatingButtonPosition(); event.currentTarget.value = Settings.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); 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 = Utils.isBlogPostUrl(location.href); // Blog Post IDs are content IDs, not Page IDs. Keep the blog context // separate so the tree can load by Space without selecting a fake page. let pageId = isBlogPostRoute ? '' : Store.getContext().pageId; const treeCurrentPageId = pageId; target.innerHTML = `
${getLocaleText('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; ContentSessionCache.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 ? '▾' : '▸'; toggle.title = isExpanded ? getLocaleText('collapseChildren') : getLocaleText('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 = getLocaleText('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 = getLocaleText('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 = getLocaleText('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) { const previousExpandedIds = treeState ? [...treeState.expanded] : []; status.className = 'confluence-plus-modal-status'; status.innerText = getLocaleText('loadingSpaceTree'); treeContainer.innerHTML = ''; try { let space = null; if (!Utils.isValidPageId(pageId)) { const spaceMatch = location.pathname.match( /\/spaces\/([^/]+)/i ); const spaceKeyFromUrl = spaceMatch ? decodeURIComponent(spaceMatch[1]) : ''; if (spaceKeyFromUrl) { space = await ConfluenceApi.spaces.getByKey( spaceKeyFromUrl ); pageId = String( space?.homepageId || space?.homepage?.id || '' ); } } if (!Utils.isValidPageId(pageId)) { throw new Error(getLocaleText('invalidPageId')); } const currentPage = await ConfluenceApi.pages.get(pageId); const spaceId = currentPage.spaceId || space?.id; if (!spaceId) { throw new Error(getLocaleText('missingSpaceId')); } if (!space || String(space.id) !== String(spaceId)) { space = await ConfluenceApi.spaces.get(spaceId); } if (!isBlogPostRoute) { Store.setContext({ pageId }); } const spaceName = (space.name || '').trim(); currentSpaceId = String(spaceId); currentSpaceName = spaceName; currentSpaceKey = (space.key || '').trim(); if (!spaceName || !currentSpaceKey) { throw new Error(getLocaleText('missingSpaceInfo')); } const cached = forceRefresh ? null : ContentSessionCache.getSpaceTree(currentSpaceId); if (forceRefresh) { ContentSessionCache.clearSpaceTree(currentSpaceId); } loadedPages = Array.isArray(cached?.pages) ? cached.pages : await SpaceTreeService.load(spaceId); treeState = SpaceTreeService.buildTree( loadedPages, treeCurrentPageId, spaceName ); const expandedIds = Array.isArray(cached?.expandedIds) ? cached.expandedIds : forceRefresh ? previousExpandedIds : null; if (Array.isArray(expandedIds)) { const validExpandedIds = expandedIds.filter((id) => treeState.nodes.has(String(id)) ); treeState.expanded = new Set( validExpandedIds.map((id) => String(id)) ); } renderTree(); persistTreeCache(); status.className = 'confluence-plus-modal-status success'; status.innerText = cached ? getLocaleText('loadedPagesFromCache', { count: treeState.filteredCount }) : getLocaleText('loadedPages', { count: treeState.filteredCount }); } catch (error) { status.className = 'confluence-plus-modal-status error'; status.innerText = error.message; Logger.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 = Store.getContext().pageId; if (!Utils.isValidPageId(pageId)) { target.innerHTML = '
' + getLocaleText('invalidPageId') + '
'; return; } target.innerHTML = `
${getLocaleText('generateSuggestionsForPage')}
${getLocaleText('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 = AIConfig.getStatus().key !== 'available'; const cachedLabelState = ContentSessionCache.getLabels(pageId); let currentPageTitle = ''; let existingLabels = Array.isArray(cachedLabelState?.existingLabels) ? [...cachedLabelState.existingLabels] : []; function persistLabelState() { const rows = [...target.querySelectorAll('.confluence-plus-label-item')]; ContentSessionCache.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 ? getLocaleText('recommendedLabelsFor', { title: pageTitle }) : getLocaleText('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'); function appendLabel(label, checked = true) { const normalized = AILabelService.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(); } ); 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); }); results.querySelector('#confluence-plus-label-add-manual').addEventListener( 'click', () => { const input = results.querySelector('#confluence-plus-label-manual'); appendLabel(input.value); input.value = ''; persistLabelState(); } ); function showLabelConfirmation(labels, anchor) { return new Promise((resolve) => { const wrapper = document.createElement('div'); const rect = anchor.getBoundingClientRect(); const width = 300; const left = Math.min( Math.max(8, rect.left), window.innerWidth - width - 8 ); wrapper.className = 'confluence-plus-popconfirm-wrap'; wrapper.style.left = `${left}px`; wrapper.style.bottom = `${window.innerHeight - rect.top + 8}px`; wrapper.innerHTML = ` `; wrapper.querySelector('.confluence-plus-popconfirm-message').innerText = getLocaleText('labelsCount', { count: labels.length, labels: labels.join(', '), }); let finished = false; function finish(value) { if (finished) return; finished = true; document.removeEventListener('click', onDocumentClick, true); wrapper.remove(); resolve(value); } function onDocumentClick(event) { if (!wrapper.contains(event.target)) { finish(false); } } wrapper.querySelector('#confluence-plus-confirm-cancel').addEventListener( 'click', () => finish(false) ); wrapper.querySelector('#confluence-plus-confirm-ok').addEventListener( 'click', () => finish(true) ); document.body.appendChild(wrapper); wrapper.querySelector('#confluence-plus-confirm-ok').focus(); setTimeout(() => { document.addEventListener( 'click', onDocumentClick, true ); }, 0); }); } results.querySelector('#confluence-plus-label-apply').addEventListener( 'click', async (event) => { const selected = [...list.querySelectorAll('.confluence-plus-label-item')] .filter((row) => row.querySelector('input[type="checkbox"]').checked) .map((row) => AILabelService.normalize( row.querySelector('.confluence-plus-label-edit').value )) .filter(Boolean); const existing = new Set( existingLabels.map((label) => AILabelService.normalize(label)) ); const labels = [...new Set(selected)].filter( (label) => !existing.has(label) ); if (!labels.length) { setLabelStatus(getLocaleText('selectNewValidLabel'), 'error'); return; } const applyButton = event.currentTarget; const confirmed = await showLabelConfirmation( labels, applyButton ); if (!confirmed) { return; } applyButton.disabled = true; setLabelStatus(getLocaleText('applyingLabels')); try { await ConfluenceApi.pages.addLabels(pageId, labels); existingLabels.push(...labels); applyButton.disabled = true; setLabelStatus( getLocaleText('addedLabelsRefreshing', { count: labels.length }), 'success' ); ContentSessionCache.clearLabels(pageId); Logger.info('ai-labels.applied', { pageId, labels }); setTimeout(() => location.reload(), 800); } catch (error) { setLabelStatus(error.message, 'error'); Logger.error('ai-labels.apply-failed', error); applyButton.disabled = false; } } ); } function localizeCachedLabelStatus(message) { const generated = String(message || '').match(/^Generated (\d+) suggestion\(s\)$/); return generated ? getLocaleText('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 (!AIConfig.isConfigured()) { setLabelStatus(getLocaleText('configureAiBeforeLabels'), 'error'); return; } generateButton.disabled = true; setLabelStatus(getLocaleText('loadingAndGenerating')); results.innerHTML = ''; persistLabelState(); try { const result = await AILabelService.generate(pageId); existingLabels = Array.isArray(result.existingLabels) ? [...result.existingLabels] : []; currentPageTitle = result.page.title || getLocaleText('currentPageBadge'); renderSuggestions(result.suggestions, currentPageTitle); setLabelStatus( result.suggestions.length ? getLocaleText('generatedSuggestions', { count: result.suggestions.length, }) : getLocaleText('noNewLabelSuggestions'), 'success' ); persistLabelState(); Logger.info('ai-labels.generated', { pageId, count: result.suggestions.length, }); } catch (error) { setLabelStatus(error.message, 'error'); Logger.error('ai-labels.generate-failed', error); } finally { generateButton.disabled = false; } }); } function openMenu() { const ui = Store.getUi(); Store.updateUi({ width: Math.max(ui.width, 720), height: Math.max(ui.height, 520), }); centerPanel(); Store.updateUi({ minimized: false }); render(); } function renderMenu() { const maximized = Store.getUi().maximized; root.innerHTML = `
${getLocaleText('contentToolTitle')} ${getLocaleText('contentToolSubtitle')}
${getLocaleText('spacePageTree')} ${getLocaleText('spacePageTreeDescription')}
${getLocaleText('generateLabel')} ${getLocaleText('aiConfigurationRequired')} ${getLocaleText('generateLabelDescription')}
`; applyContentWindowSpaceIcon(); root.querySelector('#confluence-plus-close-btn').innerHTML = windowIcons.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; // Menu navigation must not alter the panel geometry, even when a page // has very different content from the previous one. const layoutBeforeRender = preserveLayout ? { ...Store.getUi() } : null; const isContentWindow = !Store.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 (Store.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) { Store.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 = Store.getContext().pageId; } } function toast(message, type = 'info') { const element = document.createElement('div'); element.className = `confluence-plus-toast ${type}`; element.innerText = message; document.body.appendChild(element); setTimeout(() => element.remove(), 2500); } function hideContextMenu() { contextMenu?.remove(); contextMenu = null; } function showContextMenu(event) { if (Store.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'); // Only replace the browser menu for Space Page Tree rows. Other areas // (including the settings dialog) must retain their normal context menu. 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 = getLocaleText('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 (Store.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 ( Store.getUi().minimized || Store.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'; Store.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( Utils.debounce(() => { if (!root || Store.getUi().minimized) return; const ui = Store.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) { // A normal window manually resized exactly to the viewport is // logically fullscreen, so the button must switch to Restore. Store.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; // A resize from the lower-right corner means the user wants a // normal window again. Keep the size and location they chose. Store.updateUi({ maximized: false, width, height, x: Math.max(0, Math.round(rect.left)), y: Math.max(0, Math.round(rect.top)), }); updateWindowChrome(); return; } Store.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 || Store.getUi().minimized) return; // Keep the expanded panel centered whenever the browser viewport // changes. A maximized panel first returns to its normal state. if (Store.getUi().maximized) { Store.updateUi({ maximized: false }); } centerPanel(); updateWindowChrome(); } function init() { createAdoStyleEntry(); root = document.createElement('div'); root.id = 'confluence-plus-root'; // The tree is rendered inside root, so delegation on the stable root // continues to cover rows that are replaced during expansion. root.addEventListener('contextmenu', showContextMenu); document.body.appendChild(root); window.addEventListener('resize', handleViewportResize); render(); enableDrag(); observeResize(); unsubscribe = Store.subscribe((type) => { if (type === 'context') { contentFeatureUi = { spaceTreeOpen: false, aiLabelsOpen: false, }; updatePageId(); } if (type === 'ui' && Store.getUi().minimized) { render(); } }); } return { init, render, toast, destroy() { unsubscribe?.(); hideContextMenu(); unbindThemeListener(); removeAdoStyleEntry(); window.removeEventListener('resize', handleViewportResize); root?.remove(); }, }; })(); /******************************************************** * STYLES ********************************************************/ GM_addStyle(` #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-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; 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); 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; } @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: 100005; width: 300px; } .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; } `); /******************************************************** * BOOTSTRAP ********************************************************/ function bootstrap() { Store.init(); Router.init(); UI.init(); Logger.info('initialized', { version: GM_info.script.version, origin: location.origin, }); } bootstrap(); })();