@opentiny/next-sdk 0.4.1 → 0.4.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{SimulatorMask-BHVXyogh-iNUb4dyF.js → SimulatorMask-BHVXyogh-CKmEDsYG.js} +1 -1
- package/dist/core.js +1 -1
- package/dist/{index-CXK8CcKq.js → index-CHMa2zG4.js} +1985 -1434
- package/dist/index.d.ts +11 -0
- package/dist/index.js +69 -50
- package/dist/{initialize-builtin-WebMCP-rALfqS-o.js → initialize-builtin-WebMCP-JaoKwVlm.js} +355 -346
- package/dist/page-tools/a11y/build.d.ts +3 -4
- package/dist/page-tools/a11y/config.d.ts +99 -0
- package/dist/page-tools/a11y/constants.d.ts +22 -0
- package/dist/page-tools/a11y/search.d.ts +1 -1
- package/dist/page-tools/a11y/types.d.ts +13 -22
- package/dist/page-tools/a11y/utils.d.ts +46 -11
- package/dist/page-tools/a11y/vnode.d.ts +14 -11
- package/dist/page-tools/a11y-tree.d.ts +1 -0
- package/dist/page-tools/configs/console-cloud.d.ts +6 -0
- package/dist/page-tools/constants.d.ts +7 -17
- package/dist/page-tools/context.d.ts +14 -0
- package/dist/page-tools/handlers/hover.d.ts +9 -0
- package/dist/page-tools/handlers/searchTree.d.ts +2 -1
- package/dist/page-tools/page-agent-highlight/index.d.ts +12 -0
- package/dist/page-tools/page-agent-mask/SimulatorMask.d.ts +2 -0
- package/dist/page-tools/page-agent-tool-event.d.ts +26 -0
- package/dist/page-tools/schema.d.ts +4 -3
- package/dist/page-tools/tool-config.d.ts +50 -0
- package/dist/page-tools/utils/dom.d.ts +8 -2
- package/dist/runtime.d.ts +4 -1
- package/dist/runtime.js +22 -187
- package/dist/vitest.config.d.ts +2 -0
- package/package.json +26 -16
- package/WebMcpClient.ts +0 -505
- package/WebMcpServer.ts +0 -447
- package/agent/AgentModelProvider.ts +0 -932
- package/agent/type.ts +0 -91
- package/agent/utils/generateReActPrompt.ts +0 -71
- package/agent/utils/getAISDKTools.ts +0 -34
- package/agent/utils/getBuiltinMcpTools.ts +0 -86
- package/agent/utils/parseReActAction.ts +0 -34
- package/core.ts +0 -26
- package/index.ts +0 -56
- package/page-tools/a11y/build.ts +0 -70
- package/page-tools/a11y/constants.ts +0 -110
- package/page-tools/a11y/search.ts +0 -129
- package/page-tools/a11y/types.ts +0 -108
- package/page-tools/a11y/utils.ts +0 -266
- package/page-tools/a11y/vnode.ts +0 -229
- package/page-tools/a11y-tree.ts +0 -13
- package/page-tools/bridge.ts +0 -702
- package/page-tools/constants.ts +0 -72
- package/page-tools/context.ts +0 -13
- package/page-tools/effects.ts +0 -345
- package/page-tools/handlers/browserState.ts +0 -12
- package/page-tools/handlers/click.ts +0 -26
- package/page-tools/handlers/executeJavascript.ts +0 -22
- package/page-tools/handlers/fill.ts +0 -60
- package/page-tools/handlers/scroll.ts +0 -66
- package/page-tools/handlers/searchTree.ts +0 -30
- package/page-tools/handlers/select.ts +0 -34
- package/page-tools/initialize-builtin-WebMCP.ts +0 -20
- package/page-tools/page-agent-highlight/index.ts +0 -198
- package/page-tools/page-agent-mask/SimulatorMask.module.css +0 -14
- package/page-tools/page-agent-mask/SimulatorMask.ts +0 -279
- package/page-tools/page-agent-mask/checkDarkMode.ts +0 -181
- package/page-tools/page-agent-mask/cursor-border.svg +0 -3
- package/page-tools/page-agent-mask/cursor-fill.svg +0 -5
- package/page-tools/page-agent-mask/cursor.module.css +0 -70
- package/page-tools/page-agent-prompt.md +0 -139
- package/page-tools/page-agent-tool.ts +0 -175
- package/page-tools/page-state-cache.ts +0 -78
- package/page-tools/schema.ts +0 -51
- package/page-tools/utils/dom.ts +0 -155
- package/page-tools/utils/scroll.ts +0 -58
- package/remoter/QrCode.ts +0 -54
- package/remoter/createRemoter.ts +0 -957
- package/remoter/svgs/chat.svg +0 -43
- package/remoter/svgs/icon-copy.svg +0 -25
- package/remoter/svgs/link.svg +0 -28
- package/remoter/svgs/logo.svg +0 -45
- package/remoter/svgs/qrcode.svg +0 -35
- package/remoter/svgs/scan.svg +0 -21
- package/remoter/tooltips.ts +0 -260
- package/runtime.html +0 -39
- package/runtime.ts +0 -13
- package/script/utils.ts +0 -26
- package/skills/index.ts +0 -287
- package/tsconfig.json +0 -16
- package/utils/builtinProxy.ts +0 -144
- package/utils/env.ts +0 -13
- package/utils/uuid.ts +0 -10
- package/vite.config.runtime.ts +0 -22
- package/vite.config.ts +0 -63
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import { initializeWebMCPPolyfill } from '@mcp-b/webmcp-polyfill'
|
|
2
|
-
import { setupModelContextBridge } from './bridge'
|
|
3
|
-
import { isBrowser } from '../utils/env'
|
|
4
|
-
|
|
5
|
-
let initialized = false
|
|
6
|
-
|
|
7
|
-
export const initializeBuiltinWebMCP = () => {
|
|
8
|
-
if (isBrowser()) {
|
|
9
|
-
try {
|
|
10
|
-
if (initialized) return
|
|
11
|
-
|
|
12
|
-
initializeWebMCPPolyfill()
|
|
13
|
-
setupModelContextBridge()
|
|
14
|
-
|
|
15
|
-
initialized = true
|
|
16
|
-
} catch (err) {
|
|
17
|
-
console.warn('[next-sdk] 自动注入 modelContext polyfill 和桥接同步失败:', err)
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
}
|
|
@@ -1,198 +0,0 @@
|
|
|
1
|
-
import { type RefMap } from '../a11y-tree'
|
|
2
|
-
|
|
3
|
-
export const HIGHLIGHT_CONTAINER_ID = 'webmcpcli-highlight-container'
|
|
4
|
-
export const HIGHLIGHT_CONTAINER_STYLE_ID = 'webmcpcli-highlight-container-style'
|
|
5
|
-
const colors = [
|
|
6
|
-
'#FF0000',
|
|
7
|
-
'#00FF00',
|
|
8
|
-
'#0000FF',
|
|
9
|
-
'#FFA500',
|
|
10
|
-
'#800080',
|
|
11
|
-
'#008080',
|
|
12
|
-
'#FF69B4',
|
|
13
|
-
'#4B0082',
|
|
14
|
-
'#FF4500',
|
|
15
|
-
'#2E8B57',
|
|
16
|
-
'#DC143C',
|
|
17
|
-
'#4682B4'
|
|
18
|
-
]
|
|
19
|
-
// border + label opacity
|
|
20
|
-
const opacity = Math.floor(0.3 * 255)
|
|
21
|
-
.toString(16)
|
|
22
|
-
.padStart(2, '0')
|
|
23
|
-
|
|
24
|
-
const injectStyles = `
|
|
25
|
-
#${HIGHLIGHT_CONTAINER_ID} {
|
|
26
|
-
position: fixed;
|
|
27
|
-
pointer-events: none;
|
|
28
|
-
top: 0;
|
|
29
|
-
left: 0;
|
|
30
|
-
width: 100%;
|
|
31
|
-
height: 100%;
|
|
32
|
-
z-index: 2147483640;
|
|
33
|
-
background-color: transparent;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
#${HIGHLIGHT_CONTAINER_ID} div {
|
|
37
|
-
position: fixed;
|
|
38
|
-
pointer-events: none;
|
|
39
|
-
background-color: transparent;
|
|
40
|
-
text-align: right;
|
|
41
|
-
box-sizing: border-box;
|
|
42
|
-
border-radius: 4px;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
#${HIGHLIGHT_CONTAINER_ID} div span {
|
|
46
|
-
display: inline-block;
|
|
47
|
-
color: #fff;
|
|
48
|
-
padding: 0 2px;
|
|
49
|
-
border-radius: 4px;
|
|
50
|
-
}
|
|
51
|
-
`
|
|
52
|
-
|
|
53
|
-
function isElementViewport(element: HTMLElement) {
|
|
54
|
-
const rect = element.getBoundingClientRect()
|
|
55
|
-
return (
|
|
56
|
-
rect.top >= 0 &&
|
|
57
|
-
rect.left >= 0 &&
|
|
58
|
-
rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
|
|
59
|
-
rect.right <= (window.innerWidth || document.documentElement.clientWidth)
|
|
60
|
-
)
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
const doHighlight = (refMap: RefMap, parentIframe: HTMLIFrameElement | null = null) => {
|
|
64
|
-
// 1、保证注入脚本
|
|
65
|
-
if (!document.head.querySelector(`#${HIGHLIGHT_CONTAINER_STYLE_ID}`)) {
|
|
66
|
-
const style = document.head.appendChild(document.createElement('style'))
|
|
67
|
-
style.id = HIGHLIGHT_CONTAINER_STYLE_ID
|
|
68
|
-
style.textContent = injectStyles
|
|
69
|
-
document.head.appendChild(style)
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
// 2、保证contrainer( container 在 a11y 时已加入黑名单)
|
|
73
|
-
let container = document.getElementById(HIGHLIGHT_CONTAINER_ID)
|
|
74
|
-
if (!container) {
|
|
75
|
-
container = document.createElement('div')
|
|
76
|
-
container.id = HIGHLIGHT_CONTAINER_ID
|
|
77
|
-
|
|
78
|
-
document.body.appendChild(container)
|
|
79
|
-
} else {
|
|
80
|
-
container.innerHTML = ''
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
let iframeOffset = { x: 0, y: 0 }
|
|
84
|
-
if (parentIframe) {
|
|
85
|
-
const iframeRect = parentIframe.getBoundingClientRect() // Keep getBoundingClientRect for iframe offset
|
|
86
|
-
iframeOffset.x = iframeRect.left
|
|
87
|
-
iframeOffset.y = iframeRect.top
|
|
88
|
-
}
|
|
89
|
-
const fragment = document.createDocumentFragment()
|
|
90
|
-
const overlays = []
|
|
91
|
-
|
|
92
|
-
// 3、 遍历 refMap
|
|
93
|
-
for (const [index, el] of refMap.entries()) {
|
|
94
|
-
if (!isElementViewport(el)) continue
|
|
95
|
-
|
|
96
|
-
const color = colors[index % colors.length]
|
|
97
|
-
const textColor = color + opacity
|
|
98
|
-
|
|
99
|
-
const rect = el.getBoundingClientRect()
|
|
100
|
-
if (rect.width === 0 || rect.height === 0) continue
|
|
101
|
-
|
|
102
|
-
const overlay = document.createElement('div')
|
|
103
|
-
overlay.style.border = `2px solid ${textColor}`
|
|
104
|
-
|
|
105
|
-
const top = rect.top + iframeOffset.y
|
|
106
|
-
const left = rect.left + iframeOffset.x
|
|
107
|
-
|
|
108
|
-
overlay.style.top = `${top}px`
|
|
109
|
-
overlay.style.left = `${left}px`
|
|
110
|
-
overlay.style.width = `${rect.width}px`
|
|
111
|
-
overlay.style.height = `${rect.height}px`
|
|
112
|
-
|
|
113
|
-
// 文字
|
|
114
|
-
const text = document.createElement('span')
|
|
115
|
-
text.style.backgroundColor = textColor
|
|
116
|
-
text.style.height = `${rect.height - 4}px`
|
|
117
|
-
text.style.lineHeight = `${rect.height - 4}px`
|
|
118
|
-
text.textContent = index.toString()
|
|
119
|
-
overlay.appendChild(text)
|
|
120
|
-
|
|
121
|
-
fragment.appendChild(overlay)
|
|
122
|
-
overlays.push({ element: overlay, initialRect: rect })
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
container.appendChild(fragment)
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
export const highlight = (refMap: RefMap, parentIframe: HTMLIFrameElement | null = null) => {
|
|
129
|
-
doHighlight(refMap, parentIframe)
|
|
130
|
-
|
|
131
|
-
// 4、监听滚动和resize事件,更新位置
|
|
132
|
-
const updatePositions = () => doHighlight(refMap, parentIframe)
|
|
133
|
-
const throttleFunction = (func: (...args: any[]) => void, delay: number) => {
|
|
134
|
-
let lastCall = 0
|
|
135
|
-
return (...args: any[]) => {
|
|
136
|
-
const now = performance.now()
|
|
137
|
-
if (now - lastCall < delay) return
|
|
138
|
-
lastCall = now
|
|
139
|
-
return func(...args)
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
const throttledUpdatePositions = throttleFunction(updatePositions, 16)
|
|
144
|
-
window.addEventListener('scroll', throttledUpdatePositions, true)
|
|
145
|
-
window.addEventListener('resize', throttledUpdatePositions)
|
|
146
|
-
|
|
147
|
-
const cleanupFn = () => {
|
|
148
|
-
window.removeEventListener('scroll', throttledUpdatePositions, true)
|
|
149
|
-
window.removeEventListener('resize', throttledUpdatePositions)
|
|
150
|
-
}
|
|
151
|
-
const win = window as any
|
|
152
|
-
;(win._highlightCleanupFunctions = win._highlightCleanupFunctions || []).push(cleanupFn)
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
/** 移除高亮, 可反复调用 */
|
|
156
|
-
export const unhighlight = () => {
|
|
157
|
-
document.getElementById(HIGHLIGHT_CONTAINER_ID)?.remove()
|
|
158
|
-
|
|
159
|
-
const cleanupFunctions = (window as any)._highlightCleanupFunctions || []
|
|
160
|
-
for (const cleanup of cleanupFunctions) {
|
|
161
|
-
if (typeof cleanup === 'function') {
|
|
162
|
-
cleanup()
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
;(window as any)._highlightCleanupFunctions = []
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
/** 全局监听变化,随时准备移除高亮, 可反复调用 */
|
|
170
|
-
export const globalRemoveListener = () => {
|
|
171
|
-
if ((window as any).__registerGlobalRemoveListener) return
|
|
172
|
-
;(window as any).__registerGlobalRemoveListener = true
|
|
173
|
-
window.addEventListener('popstate', () => {
|
|
174
|
-
unhighlight()
|
|
175
|
-
})
|
|
176
|
-
window.addEventListener('hashchange', () => {
|
|
177
|
-
unhighlight()
|
|
178
|
-
})
|
|
179
|
-
window.addEventListener('beforeunload', () => {
|
|
180
|
-
unhighlight()
|
|
181
|
-
})
|
|
182
|
-
|
|
183
|
-
const navigation = (window as any).navigation
|
|
184
|
-
if (navigation && typeof navigation.addEventListener === 'function') {
|
|
185
|
-
navigation.addEventListener('navigate', () => {
|
|
186
|
-
unhighlight()
|
|
187
|
-
})
|
|
188
|
-
} else {
|
|
189
|
-
// 定时器
|
|
190
|
-
let currentUrl = window.location.href
|
|
191
|
-
setInterval(() => {
|
|
192
|
-
if (window.location.href !== currentUrl) {
|
|
193
|
-
currentUrl = window.location.href
|
|
194
|
-
unhighlight()
|
|
195
|
-
}
|
|
196
|
-
}, 500)
|
|
197
|
-
}
|
|
198
|
-
}
|
|
@@ -1,279 +0,0 @@
|
|
|
1
|
-
import { Motion } from 'ai-motion'
|
|
2
|
-
|
|
3
|
-
import { isPageDark } from './checkDarkMode'
|
|
4
|
-
|
|
5
|
-
const injectStyles = `
|
|
6
|
-
.webmcp-page-agent-cursor {
|
|
7
|
-
position: absolute;
|
|
8
|
-
width: var(--cursor-size, 75px);
|
|
9
|
-
height: var(--cursor-size, 75px);
|
|
10
|
-
pointer-events: none;
|
|
11
|
-
z-index: 10000;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
.webmcp-page-agent-cursorBorder {
|
|
15
|
-
position: absolute;
|
|
16
|
-
width: 100%;
|
|
17
|
-
height: 100%;
|
|
18
|
-
background: linear-gradient(45deg, rgb(57, 182, 255), rgb(189, 69, 251));
|
|
19
|
-
mask-image: url("data:image/svg+xml,%3csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%20100%20100'%20fill='none'%3e%3cg%3e%3cpath%20d='M%2015%2042%20L%2015%2036.99%20Q%2015%2031.99%2023.7%2031.99%20L%2028.05%2031.99%20Q%2032.41%2031.99%2032.41%2021.99%20L%2032.41%2017%20Q%2032.41%2012%2041.09%2016.95%20L%2076.31%2037.05%20Q%2085%2042%2076.31%2046.95%20L%2041.09%2067.05%20Q%2032.41%2072%2032.41%2062.01%20L%2032.41%2057.01%20Q%2032.41%2052.01%2023.7%2052.01%20L%2019.35%2052.01%20Q%2015%2052.01%2015%2047.01%20Z'%20fill='none'%20stroke='%23000000'%20stroke-width='6'%20stroke-miterlimit='10'%20style='stroke:%20light-dark(rgb(0,%200,%200),%20rgb(255,%20255,%20255));'/%3e%3c/g%3e%3c/svg%3e");
|
|
20
|
-
mask-size: 100% 100%;
|
|
21
|
-
mask-repeat: no-repeat;
|
|
22
|
-
|
|
23
|
-
transform-origin: center;
|
|
24
|
-
transform: rotate(-90deg) scale(0.8);
|
|
25
|
-
margin-left: -10px;
|
|
26
|
-
margin-top: -18px;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
.webmcp-page-agent-cursorFilling {
|
|
30
|
-
position: absolute;
|
|
31
|
-
width: 100%;
|
|
32
|
-
height: 100%;
|
|
33
|
-
background: url("data:image/svg+xml,%3csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%20100%20100'%3e%3cdefs%3e%3c/defs%3e%3cg%20xmlns='http://www.w3.org/2000/svg'%20style='filter:%20drop-shadow(light-dark(rgba(0,%200,%200,%200.4),%20rgba(237,%20237,%20237,%200.4))%203px%204px%204px);'%3e%3cpath%20d='M%2015%2042%20L%2015%2036.99%20Q%2015%2031.99%2023.7%2031.99%20L%2028.05%2031.99%20Q%2032.41%2031.99%2032.41%2021.99%20L%2032.41%2017%20Q%2032.41%2012%2041.09%2016.95%20L%2076.31%2037.05%20Q%2085%2042%2076.31%2046.95%20L%2041.09%2067.05%20Q%2032.41%2072%2032.41%2062.01%20L%2032.41%2057.01%20Q%2032.41%2052.01%2023.7%2052.01%20L%2019.35%2052.01%20Q%2015%2052.01%2015%2047.01%20Z'%20fill='%23ffffff'%20stroke='none'%20style='fill:%20%23ffffff;'/%3e%3c/g%3e%3c/svg%3e");
|
|
34
|
-
background-size: 100% 100%;
|
|
35
|
-
background-repeat: no-repeat;
|
|
36
|
-
|
|
37
|
-
transform-origin: center;
|
|
38
|
-
transform: rotate(-90deg) scale(0.8);
|
|
39
|
-
margin-left: -10px;
|
|
40
|
-
margin-top: -18px;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
.webmcp-page-agent-cursorRipple {
|
|
44
|
-
position: absolute;
|
|
45
|
-
width: 100%;
|
|
46
|
-
height: 100%;
|
|
47
|
-
pointer-events: none;
|
|
48
|
-
margin-left: -50%;
|
|
49
|
-
margin-top: -50%;
|
|
50
|
-
|
|
51
|
-
&::after {
|
|
52
|
-
content: '';
|
|
53
|
-
opacity: 0;
|
|
54
|
-
position: absolute;
|
|
55
|
-
inset: 0;
|
|
56
|
-
border: 4px solid rgba(57, 182, 255, 1);
|
|
57
|
-
border-radius: 50%;
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
.webmcp-page-agent-cursor.clicking .webmcp-page-agent-cursorRipple::after {
|
|
62
|
-
animation: webmcp-page-agent-cursor-ripple 300ms ease-out forwards;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
@keyframes webmcp-page-agent-cursor-ripple {
|
|
66
|
-
0% {
|
|
67
|
-
transform: scale(0);
|
|
68
|
-
opacity: 1;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
100% {
|
|
72
|
-
transform: scale(2);
|
|
73
|
-
opacity: 0;
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
.webmcp-page-agent-wrapper {
|
|
78
|
-
position: fixed;
|
|
79
|
-
inset: 0;
|
|
80
|
-
z-index: 2147483641;
|
|
81
|
-
/* 确保在所有元素之上,除了 panel */
|
|
82
|
-
cursor: wait;
|
|
83
|
-
overflow: hidden;
|
|
84
|
-
|
|
85
|
-
display: none;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
.webmcp-page-agent-wrapper.visible {
|
|
89
|
-
display: block;
|
|
90
|
-
}
|
|
91
|
-
`
|
|
92
|
-
|
|
93
|
-
export class SimulatorMask extends EventTarget {
|
|
94
|
-
shown: boolean = false
|
|
95
|
-
wrapper = document.createElement('div')
|
|
96
|
-
motion: Motion | null = null
|
|
97
|
-
|
|
98
|
-
#disposed = false
|
|
99
|
-
|
|
100
|
-
#cursor = document.createElement('div')
|
|
101
|
-
|
|
102
|
-
#currentCursorX = 0
|
|
103
|
-
#currentCursorY = 0
|
|
104
|
-
|
|
105
|
-
#targetCursorX = 0
|
|
106
|
-
#targetCursorY = 0
|
|
107
|
-
|
|
108
|
-
constructor() {
|
|
109
|
-
super()
|
|
110
|
-
// 注入样式,防止打包失败
|
|
111
|
-
document.head.appendChild(document.createElement('style')).textContent = injectStyles
|
|
112
|
-
this.wrapper.id = 'page-agent-runtime_simulator-mask'
|
|
113
|
-
this.wrapper.className = 'webmcp-page-agent-wrapper'
|
|
114
|
-
this.wrapper.setAttribute('data-browser-use-ignore', 'true')
|
|
115
|
-
this.wrapper.setAttribute('data-page-agent-ignore', 'true')
|
|
116
|
-
|
|
117
|
-
try {
|
|
118
|
-
const motion = new Motion({
|
|
119
|
-
mode: isPageDark() ? 'dark' : 'light',
|
|
120
|
-
styles: { position: 'absolute', inset: '0' }
|
|
121
|
-
})
|
|
122
|
-
this.motion = motion
|
|
123
|
-
this.wrapper.appendChild(motion.element)
|
|
124
|
-
motion.autoResize(this.wrapper)
|
|
125
|
-
} catch (e) {
|
|
126
|
-
console.warn('[SimulatorMask] Motion overlay unavailable:', e)
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
const stopEvent = (e: Event) => {
|
|
130
|
-
e.stopPropagation()
|
|
131
|
-
e.preventDefault()
|
|
132
|
-
}
|
|
133
|
-
;['click', 'mousedown', 'mouseup', 'mousemove', 'wheel', 'keydown', 'keyup'].forEach((eventName) =>
|
|
134
|
-
this.wrapper.addEventListener(eventName, stopEvent)
|
|
135
|
-
)
|
|
136
|
-
// Create AI cursor
|
|
137
|
-
this.#createCursor()
|
|
138
|
-
// this.show()
|
|
139
|
-
|
|
140
|
-
document.body.appendChild(this.wrapper)
|
|
141
|
-
|
|
142
|
-
this.#moveCursorToTarget()
|
|
143
|
-
|
|
144
|
-
// global events
|
|
145
|
-
// @note Mask should be isolated from the rest of the code.
|
|
146
|
-
// Global events are easier to manage and cleanup.
|
|
147
|
-
|
|
148
|
-
const movePointerToListener = (event: Event) => {
|
|
149
|
-
const { x, y } = (event as CustomEvent).detail
|
|
150
|
-
this.setCursorPosition(x, y)
|
|
151
|
-
}
|
|
152
|
-
const clickPointerListener = () => {
|
|
153
|
-
this.triggerClickAnimation()
|
|
154
|
-
}
|
|
155
|
-
const enablePassThroughListener = () => {
|
|
156
|
-
this.wrapper.style.pointerEvents = 'none'
|
|
157
|
-
}
|
|
158
|
-
const disablePassThroughListener = () => {
|
|
159
|
-
this.wrapper.style.pointerEvents = 'auto'
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
window.addEventListener('PageAgent::MovePointerTo', movePointerToListener)
|
|
163
|
-
window.addEventListener('PageAgent::ClickPointer', clickPointerListener)
|
|
164
|
-
window.addEventListener('PageAgent::EnablePassThrough', enablePassThroughListener)
|
|
165
|
-
window.addEventListener('PageAgent::DisablePassThrough', disablePassThroughListener)
|
|
166
|
-
|
|
167
|
-
this.addEventListener('dispose', () => {
|
|
168
|
-
window.removeEventListener('PageAgent::MovePointerTo', movePointerToListener)
|
|
169
|
-
window.removeEventListener('PageAgent::ClickPointer', clickPointerListener)
|
|
170
|
-
window.removeEventListener('PageAgent::EnablePassThrough', enablePassThroughListener)
|
|
171
|
-
window.removeEventListener('PageAgent::DisablePassThrough', disablePassThroughListener)
|
|
172
|
-
})
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
#createCursor() {
|
|
176
|
-
this.#cursor.className = 'webmcp-page-agent-cursor'
|
|
177
|
-
|
|
178
|
-
// Create ripple effect container
|
|
179
|
-
const rippleContainer = document.createElement('div')
|
|
180
|
-
rippleContainer.className = 'webmcp-page-agent-cursorRipple'
|
|
181
|
-
this.#cursor.appendChild(rippleContainer)
|
|
182
|
-
|
|
183
|
-
// Create filling layer
|
|
184
|
-
const fillingLayer = document.createElement('div')
|
|
185
|
-
fillingLayer.className = 'webmcp-page-agent-cursorFilling'
|
|
186
|
-
this.#cursor.appendChild(fillingLayer)
|
|
187
|
-
|
|
188
|
-
// Create border layer
|
|
189
|
-
const borderLayer = document.createElement('div')
|
|
190
|
-
borderLayer.className = 'webmcp-page-agent-cursorBorder'
|
|
191
|
-
this.#cursor.appendChild(borderLayer)
|
|
192
|
-
|
|
193
|
-
this.wrapper.appendChild(this.#cursor)
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
#moveCursorToTarget() {
|
|
197
|
-
if (this.#disposed) return
|
|
198
|
-
|
|
199
|
-
const newX = this.#currentCursorX + (this.#targetCursorX - this.#currentCursorX) * 0.2
|
|
200
|
-
const newY = this.#currentCursorY + (this.#targetCursorY - this.#currentCursorY) * 0.2
|
|
201
|
-
|
|
202
|
-
const xDistance = Math.abs(newX - this.#targetCursorX)
|
|
203
|
-
if (xDistance > 0) {
|
|
204
|
-
if (xDistance < 2) {
|
|
205
|
-
this.#currentCursorX = this.#targetCursorX
|
|
206
|
-
} else {
|
|
207
|
-
this.#currentCursorX = newX
|
|
208
|
-
}
|
|
209
|
-
this.#cursor.style.left = `${this.#currentCursorX}px`
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
const yDistance = Math.abs(newY - this.#targetCursorY)
|
|
213
|
-
if (yDistance > 0) {
|
|
214
|
-
if (yDistance < 2) {
|
|
215
|
-
this.#currentCursorY = this.#targetCursorY
|
|
216
|
-
} else {
|
|
217
|
-
this.#currentCursorY = newY
|
|
218
|
-
}
|
|
219
|
-
this.#cursor.style.top = `${this.#currentCursorY}px`
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
requestAnimationFrame(() => this.#moveCursorToTarget())
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
setCursorPosition(x: number, y: number) {
|
|
226
|
-
if (this.#disposed) return
|
|
227
|
-
|
|
228
|
-
this.#targetCursorX = x
|
|
229
|
-
this.#targetCursorY = y
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
triggerClickAnimation() {
|
|
233
|
-
if (this.#disposed) return
|
|
234
|
-
|
|
235
|
-
this.#cursor.classList.remove('clicking')
|
|
236
|
-
// Force reflow to restart animation
|
|
237
|
-
void this.#cursor.offsetHeight
|
|
238
|
-
this.#cursor.classList.add('clicking')
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
show() {
|
|
242
|
-
if (this.shown || this.#disposed) return
|
|
243
|
-
|
|
244
|
-
this.shown = true
|
|
245
|
-
this.motion?.start()
|
|
246
|
-
this.motion?.fadeIn()
|
|
247
|
-
|
|
248
|
-
this.wrapper.classList.add('visible')
|
|
249
|
-
|
|
250
|
-
// Initialize cursor position
|
|
251
|
-
this.#currentCursorX = window.innerWidth / 2
|
|
252
|
-
this.#currentCursorY = window.innerHeight / 2
|
|
253
|
-
this.#targetCursorX = this.#currentCursorX
|
|
254
|
-
this.#targetCursorY = this.#currentCursorY
|
|
255
|
-
this.#cursor.style.left = `${this.#currentCursorX}px`
|
|
256
|
-
this.#cursor.style.top = `${this.#currentCursorY}px`
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
hide() {
|
|
260
|
-
if (!this.shown || this.#disposed) return
|
|
261
|
-
|
|
262
|
-
this.shown = false
|
|
263
|
-
this.motion?.fadeOut()
|
|
264
|
-
this.motion?.pause()
|
|
265
|
-
|
|
266
|
-
this.#cursor.classList.remove('clicking')
|
|
267
|
-
|
|
268
|
-
setTimeout(() => {
|
|
269
|
-
this.wrapper.classList.remove('visible')
|
|
270
|
-
}, 800) // Match the animation duration
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
dispose() {
|
|
274
|
-
this.#disposed = true
|
|
275
|
-
this.motion?.dispose()
|
|
276
|
-
this.wrapper.remove()
|
|
277
|
-
this.dispatchEvent(new Event('dispose'))
|
|
278
|
-
}
|
|
279
|
-
}
|
|
@@ -1,181 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* A comprehensive function to determine if the page is currently in a dark theme.
|
|
3
|
-
* Heuristic check. Only work for common patterns. Return false by default.
|
|
4
|
-
*/
|
|
5
|
-
export function isPageDark() {
|
|
6
|
-
try {
|
|
7
|
-
if (hasDarkModeClass()) return true
|
|
8
|
-
if (hasDarkModeDataAttribute()) return true
|
|
9
|
-
if (isColorSchemeDark()) return true
|
|
10
|
-
if (isBackgroundDark()) return true
|
|
11
|
-
if (isMainContentBackgroundDark()) return true
|
|
12
|
-
if (isTextColorLight()) return true
|
|
13
|
-
|
|
14
|
-
return false
|
|
15
|
-
} catch (error) {
|
|
16
|
-
console.warn('Error determining if page is dark:', error)
|
|
17
|
-
return false
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Checks for common dark mode CSS classes on the html or body elements.
|
|
23
|
-
*/
|
|
24
|
-
function hasDarkModeClass() {
|
|
25
|
-
const DEFAULT_DARK_MODE_CLASSES = ['dark', 'dark-mode', 'theme-dark', 'night', 'night-mode']
|
|
26
|
-
|
|
27
|
-
const htmlElement = document.documentElement
|
|
28
|
-
const bodyElement = document.body || document.documentElement // can be null in some cases
|
|
29
|
-
|
|
30
|
-
// Check class names on <html> and <body>
|
|
31
|
-
for (const className of DEFAULT_DARK_MODE_CLASSES) {
|
|
32
|
-
if (htmlElement.classList.contains(className) || bodyElement?.classList.contains(className)) {
|
|
33
|
-
return true
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
return false
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Some UI frameworks use data attributes to indicate theme
|
|
42
|
-
*/
|
|
43
|
-
function hasDarkModeDataAttribute() {
|
|
44
|
-
const htmlElement = document.documentElement
|
|
45
|
-
const bodyElement = document.body || document.documentElement // can be null in some cases
|
|
46
|
-
|
|
47
|
-
const dataAttrs = ['data-theme', 'data-color-mode', 'data-bs-theme', 'data-mui-color-scheme']
|
|
48
|
-
for (const attr of dataAttrs) {
|
|
49
|
-
const bodyValue = bodyElement?.getAttribute(attr)
|
|
50
|
-
const htmlValue = htmlElement.getAttribute(attr)
|
|
51
|
-
|
|
52
|
-
if (bodyValue?.toLowerCase() === 'dark' || htmlValue?.toLowerCase() === 'dark') {
|
|
53
|
-
return true
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
return false
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* Checks the CSS `color-scheme` property and `<meta name="color-scheme">` tag.
|
|
62
|
-
* Only "dark"/"only dark" counts as dark; "light dark" is ambiguous and ignored.
|
|
63
|
-
*/
|
|
64
|
-
function isColorSchemeDark() {
|
|
65
|
-
// Check <meta name="color-scheme" content="dark">
|
|
66
|
-
const meta = document.querySelector<HTMLMetaElement>('meta[name="color-scheme"]')
|
|
67
|
-
const metaContent = meta?.content.toLowerCase()
|
|
68
|
-
if (metaContent === 'dark' || metaContent === 'only dark') return true
|
|
69
|
-
|
|
70
|
-
// Check the computed color-scheme CSS property on :root
|
|
71
|
-
const rootStyle = window.getComputedStyle(document.documentElement)
|
|
72
|
-
const colorScheme = rootStyle.getPropertyValue('color-scheme').trim().toLowerCase()
|
|
73
|
-
return colorScheme === 'dark' || colorScheme === 'only dark'
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* Checks the background color of the body element to determine if the page is dark.
|
|
78
|
-
*/
|
|
79
|
-
function isBackgroundDark() {
|
|
80
|
-
// We check both <html> and <body> because some pages set the color on <html>
|
|
81
|
-
const htmlStyle = window.getComputedStyle(document.documentElement)
|
|
82
|
-
const bodyStyle = window.getComputedStyle(document.body || document.documentElement)
|
|
83
|
-
|
|
84
|
-
// Get background colors
|
|
85
|
-
const htmlBgColor = htmlStyle.backgroundColor
|
|
86
|
-
const bodyBgColor = bodyStyle.backgroundColor
|
|
87
|
-
|
|
88
|
-
// The body's background might be transparent, in which case we should
|
|
89
|
-
// fall back to the html element's background.
|
|
90
|
-
if (isColorDark(bodyBgColor)) {
|
|
91
|
-
return true
|
|
92
|
-
} else if (bodyBgColor === 'transparent' || bodyBgColor.startsWith('rgba(0, 0, 0, 0)')) {
|
|
93
|
-
return isColorDark(htmlBgColor)
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
return false
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
* Checks if the text color on the body is light, which implies a dark background.
|
|
101
|
-
*/
|
|
102
|
-
function isTextColorLight() {
|
|
103
|
-
/** Luminance (0-255) above which body text is considered light */
|
|
104
|
-
const LIGHT_TEXT_LUMINANCE = 200
|
|
105
|
-
|
|
106
|
-
const bodyStyle = window.getComputedStyle(document.body || document.documentElement)
|
|
107
|
-
const luminance = getLuminance(bodyStyle.color)
|
|
108
|
-
|
|
109
|
-
// Light text has high luminance (e.g. white text on dark bg)
|
|
110
|
-
return luminance !== null && luminance > LIGHT_TEXT_LUMINANCE
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
/**
|
|
114
|
-
* Checks the background color of major layout elements (#app, #root, etc.).
|
|
115
|
-
* Many SPAs render into a container that may have its own dark background while
|
|
116
|
-
* <body> remains transparent.
|
|
117
|
-
*/
|
|
118
|
-
function isMainContentBackgroundDark() {
|
|
119
|
-
const { innerWidth: vw, innerHeight: vh } = window
|
|
120
|
-
const minArea = vw * vh * 0.5
|
|
121
|
-
|
|
122
|
-
const selectors = ['#app', '#root', '#__next']
|
|
123
|
-
for (const selector of selectors) {
|
|
124
|
-
const el = document.querySelector(selector)
|
|
125
|
-
if (!el) continue
|
|
126
|
-
|
|
127
|
-
const rect = el.getBoundingClientRect()
|
|
128
|
-
if (rect.width * rect.height < minArea) continue
|
|
129
|
-
|
|
130
|
-
if (isColorDark(window.getComputedStyle(el).backgroundColor)) return true
|
|
131
|
-
}
|
|
132
|
-
return false
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
// --- utils ---
|
|
136
|
-
|
|
137
|
-
/**
|
|
138
|
-
* Parses an RGB or RGBA color string and returns an object with r, g, b properties.
|
|
139
|
-
* @param {string} colorString - e.g., "rgb(34, 34, 34)" or "rgba(0, 0, 0, 0.5)"
|
|
140
|
-
* @returns {{r: number, g: number, b: number}|null}
|
|
141
|
-
*/
|
|
142
|
-
function parseRgbColor(colorString: string) {
|
|
143
|
-
const rgbMatch = /rgba?\((\d+),\s*(\d+),\s*(\d+)/.exec(colorString)
|
|
144
|
-
if (!rgbMatch) {
|
|
145
|
-
return null // Not a valid rgb/rgba string
|
|
146
|
-
}
|
|
147
|
-
return {
|
|
148
|
-
r: parseInt(rgbMatch[1]),
|
|
149
|
-
g: parseInt(rgbMatch[2]),
|
|
150
|
-
b: parseInt(rgbMatch[3]),
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
/**
|
|
155
|
-
* Calculates the perceived luminance (0-255) of a CSS color string.
|
|
156
|
-
* @param {string} colorString - e.g., "rgb(50, 50, 50)" or "rgba(0, 0, 0, 0.5)"
|
|
157
|
-
* @returns {number|null} - The luminance, or null if the color is transparent or unparseable.
|
|
158
|
-
*/
|
|
159
|
-
function getLuminance(colorString: string): number | null {
|
|
160
|
-
if (!colorString || colorString === 'transparent' || colorString.startsWith('rgba(0, 0, 0, 0)')) {
|
|
161
|
-
return null // Transparent has no meaningful luminance
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
const rgb = parseRgbColor(colorString)
|
|
165
|
-
if (!rgb) {
|
|
166
|
-
return null // Could not parse color
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
// Standard perceived luminance formula
|
|
170
|
-
return 0.299 * rgb.r + 0.587 * rgb.g + 0.114 * rgb.b
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
/**
|
|
174
|
-
* Determines if a color is "dark" based on its calculated luminance.
|
|
175
|
-
* @param {string} colorString - The CSS color string (e.g., "rgb(50, 50, 50)").
|
|
176
|
-
* @param {number} threshold - A value between 0 and 255. Colors with luminance below this will be considered dark. Default is 128.
|
|
177
|
-
*/
|
|
178
|
-
function isColorDark(colorString: string, threshold = 128) {
|
|
179
|
-
const luminance = getLuminance(colorString)
|
|
180
|
-
return luminance !== null && luminance < threshold
|
|
181
|
-
}
|
|
@@ -1,3 +0,0 @@
|
|
|
1
|
-
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" fill="none">
|
|
2
|
-
<g><path d="M 15 42 L 15 36.99 Q 15 31.99 23.7 31.99 L 28.05 31.99 Q 32.41 31.99 32.41 21.99 L 32.41 17 Q 32.41 12 41.09 16.95 L 76.31 37.05 Q 85 42 76.31 46.95 L 41.09 67.05 Q 32.41 72 32.41 62.01 L 32.41 57.01 Q 32.41 52.01 23.7 52.01 L 19.35 52.01 Q 15 52.01 15 47.01 Z" fill="none" stroke="#000000" stroke-width="6" stroke-miterlimit="10" style="stroke: light-dark(rgb(0, 0, 0), rgb(255, 255, 255));"/></g>
|
|
3
|
-
</svg>
|