@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.
Files changed (90) hide show
  1. package/dist/{SimulatorMask-BHVXyogh-iNUb4dyF.js → SimulatorMask-BHVXyogh-CKmEDsYG.js} +1 -1
  2. package/dist/core.js +1 -1
  3. package/dist/{index-CXK8CcKq.js → index-CHMa2zG4.js} +1985 -1434
  4. package/dist/index.d.ts +11 -0
  5. package/dist/index.js +69 -50
  6. package/dist/{initialize-builtin-WebMCP-rALfqS-o.js → initialize-builtin-WebMCP-JaoKwVlm.js} +355 -346
  7. package/dist/page-tools/a11y/build.d.ts +3 -4
  8. package/dist/page-tools/a11y/config.d.ts +99 -0
  9. package/dist/page-tools/a11y/constants.d.ts +22 -0
  10. package/dist/page-tools/a11y/search.d.ts +1 -1
  11. package/dist/page-tools/a11y/types.d.ts +13 -22
  12. package/dist/page-tools/a11y/utils.d.ts +46 -11
  13. package/dist/page-tools/a11y/vnode.d.ts +14 -11
  14. package/dist/page-tools/a11y-tree.d.ts +1 -0
  15. package/dist/page-tools/configs/console-cloud.d.ts +6 -0
  16. package/dist/page-tools/constants.d.ts +7 -17
  17. package/dist/page-tools/context.d.ts +14 -0
  18. package/dist/page-tools/handlers/hover.d.ts +9 -0
  19. package/dist/page-tools/handlers/searchTree.d.ts +2 -1
  20. package/dist/page-tools/page-agent-highlight/index.d.ts +12 -0
  21. package/dist/page-tools/page-agent-mask/SimulatorMask.d.ts +2 -0
  22. package/dist/page-tools/page-agent-tool-event.d.ts +26 -0
  23. package/dist/page-tools/schema.d.ts +4 -3
  24. package/dist/page-tools/tool-config.d.ts +50 -0
  25. package/dist/page-tools/utils/dom.d.ts +8 -2
  26. package/dist/runtime.d.ts +4 -1
  27. package/dist/runtime.js +22 -187
  28. package/dist/vitest.config.d.ts +2 -0
  29. package/package.json +26 -16
  30. package/WebMcpClient.ts +0 -505
  31. package/WebMcpServer.ts +0 -447
  32. package/agent/AgentModelProvider.ts +0 -932
  33. package/agent/type.ts +0 -91
  34. package/agent/utils/generateReActPrompt.ts +0 -71
  35. package/agent/utils/getAISDKTools.ts +0 -34
  36. package/agent/utils/getBuiltinMcpTools.ts +0 -86
  37. package/agent/utils/parseReActAction.ts +0 -34
  38. package/core.ts +0 -26
  39. package/index.ts +0 -56
  40. package/page-tools/a11y/build.ts +0 -70
  41. package/page-tools/a11y/constants.ts +0 -110
  42. package/page-tools/a11y/search.ts +0 -129
  43. package/page-tools/a11y/types.ts +0 -108
  44. package/page-tools/a11y/utils.ts +0 -266
  45. package/page-tools/a11y/vnode.ts +0 -229
  46. package/page-tools/a11y-tree.ts +0 -13
  47. package/page-tools/bridge.ts +0 -702
  48. package/page-tools/constants.ts +0 -72
  49. package/page-tools/context.ts +0 -13
  50. package/page-tools/effects.ts +0 -345
  51. package/page-tools/handlers/browserState.ts +0 -12
  52. package/page-tools/handlers/click.ts +0 -26
  53. package/page-tools/handlers/executeJavascript.ts +0 -22
  54. package/page-tools/handlers/fill.ts +0 -60
  55. package/page-tools/handlers/scroll.ts +0 -66
  56. package/page-tools/handlers/searchTree.ts +0 -30
  57. package/page-tools/handlers/select.ts +0 -34
  58. package/page-tools/initialize-builtin-WebMCP.ts +0 -20
  59. package/page-tools/page-agent-highlight/index.ts +0 -198
  60. package/page-tools/page-agent-mask/SimulatorMask.module.css +0 -14
  61. package/page-tools/page-agent-mask/SimulatorMask.ts +0 -279
  62. package/page-tools/page-agent-mask/checkDarkMode.ts +0 -181
  63. package/page-tools/page-agent-mask/cursor-border.svg +0 -3
  64. package/page-tools/page-agent-mask/cursor-fill.svg +0 -5
  65. package/page-tools/page-agent-mask/cursor.module.css +0 -70
  66. package/page-tools/page-agent-prompt.md +0 -139
  67. package/page-tools/page-agent-tool.ts +0 -175
  68. package/page-tools/page-state-cache.ts +0 -78
  69. package/page-tools/schema.ts +0 -51
  70. package/page-tools/utils/dom.ts +0 -155
  71. package/page-tools/utils/scroll.ts +0 -58
  72. package/remoter/QrCode.ts +0 -54
  73. package/remoter/createRemoter.ts +0 -957
  74. package/remoter/svgs/chat.svg +0 -43
  75. package/remoter/svgs/icon-copy.svg +0 -25
  76. package/remoter/svgs/link.svg +0 -28
  77. package/remoter/svgs/logo.svg +0 -45
  78. package/remoter/svgs/qrcode.svg +0 -35
  79. package/remoter/svgs/scan.svg +0 -21
  80. package/remoter/tooltips.ts +0 -260
  81. package/runtime.html +0 -39
  82. package/runtime.ts +0 -13
  83. package/script/utils.ts +0 -26
  84. package/skills/index.ts +0 -287
  85. package/tsconfig.json +0 -16
  86. package/utils/builtinProxy.ts +0 -144
  87. package/utils/env.ts +0 -13
  88. package/utils/uuid.ts +0 -10
  89. package/vite.config.runtime.ts +0 -22
  90. 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,14 +0,0 @@
1
- .webmcp-page-agent-wrapper {
2
- position: fixed;
3
- inset: 0;
4
- z-index: 2147483641;
5
- /* 确保在所有元素之上,除了 panel */
6
- cursor: wait;
7
- overflow: hidden;
8
-
9
- display: none;
10
- }
11
-
12
- .webmcp-page-agent-wrapper.visible {
13
- display: block;
14
- }
@@ -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>