@electerm/electerm-react 2.3.6 → 2.3.18
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/client/common/constants.js +1 -1
- package/client/common/ui-theme.js +25 -12
- package/client/components/ai/ai.styl +1 -3
- package/client/components/bg/custom-css.jsx +2 -2
- package/client/components/common/highlight.styl +1 -2
- package/client/components/common/input-context-menu.jsx +294 -0
- package/client/components/file-transfer/transfer.styl +5 -8
- package/client/components/footer/footer.styl +2 -5
- package/client/components/layout/layout.styl +2 -3
- package/client/components/main/error-wrapper.jsx +3 -3
- package/client/components/main/main.jsx +2 -1
- package/client/components/main/ui-theme.jsx +46 -6
- package/client/components/main/upgrade.styl +4 -6
- package/client/components/main/wrapper.styl +5 -27
- package/client/components/quick-commands/qm.styl +3 -5
- package/client/components/session/session.styl +11 -13
- package/client/components/setting-panel/list.styl +5 -5
- package/client/components/setting-panel/setting-wrap.styl +1 -6
- package/client/components/setting-panel/terminal-bg-config.jsx +3 -0
- package/client/components/setting-sync/setting-sync-form.jsx +0 -1
- package/client/components/sftp/file-item.jsx +3 -0
- package/client/components/sftp/sftp.styl +21 -26
- package/client/components/sftp/transfer-tag.styl +3 -5
- package/client/components/side-panel-r/right-side-panel.styl +7 -9
- package/client/components/sidebar/info.styl +0 -12
- package/client/components/sidebar/sidebar.styl +15 -16
- package/client/components/sys-menu/sys-menu.styl +8 -10
- package/client/components/tabs/tabs.styl +29 -31
- package/client/components/terminal/term-search.styl +3 -3
- package/client/components/terminal/terminal.jsx +2 -2
- package/client/components/terminal/terminal.styl +21 -22
- package/client/components/terminal-info/terminal-info.styl +7 -7
- package/client/components/theme/terminal-theme-list.styl +2 -2
- package/client/components/theme/theme-list-item.jsx +62 -20
- package/client/css/basic.styl +4 -7
- package/client/css/includes/theme.styl +16 -0
- package/client/store/ui-theme.js +0 -35
- package/package.json +1 -1
- package/client/components/common/native-input.styl +0 -7
- package/client/components/setting-sync/sync.styl +0 -7
- package/client/css/antd-overwrite.styl +0 -10
- package/client/css/includes/theme-default.styl +0 -20
|
@@ -2,19 +2,32 @@
|
|
|
2
2
|
* ui theme related
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
5
|
+
const defaultUiThemeStylus = `
|
|
6
|
+
--main #141314
|
|
7
|
+
--main-dark #000
|
|
8
|
+
--main-light #2E3338
|
|
9
|
+
--text #ddd
|
|
10
|
+
--text-light #fff
|
|
11
|
+
--text-dark #888
|
|
12
|
+
--text-disabled #777
|
|
13
|
+
--primary #08c
|
|
14
|
+
--info #FFD166
|
|
15
|
+
--success #06D6A0
|
|
16
|
+
--error #EF476F
|
|
17
|
+
--warn #E55934
|
|
18
|
+
`
|
|
19
|
+
|
|
20
|
+
export function getUiThemeConfig (conf = defaultUiThemeStylus) {
|
|
21
|
+
const lines = conf.split('\n').filter(line => line.trim())
|
|
22
|
+
return lines.reduce((p, line) => {
|
|
23
|
+
const [k, v] = line.trim().replace('--', '').split(' ')
|
|
24
|
+
if (k && v) {
|
|
25
|
+
return {
|
|
26
|
+
...p,
|
|
27
|
+
[k]: v
|
|
28
|
+
}
|
|
17
29
|
}
|
|
30
|
+
return p
|
|
18
31
|
}, {})
|
|
19
32
|
}
|
|
20
33
|
|
|
@@ -1,9 +1,7 @@
|
|
|
1
|
-
@require '../../css/includes/theme-default'
|
|
2
1
|
.ai-chat-container
|
|
3
2
|
height 100%
|
|
4
3
|
display flex
|
|
5
4
|
flex-direction column
|
|
6
|
-
color text
|
|
7
5
|
|
|
8
6
|
.ai-chat-history
|
|
9
7
|
flex-grow 1
|
|
@@ -17,7 +15,7 @@
|
|
|
17
15
|
|
|
18
16
|
.chat-history-item
|
|
19
17
|
.code-block
|
|
20
|
-
border 1px dashed
|
|
18
|
+
border 1px dashed var(--main-darker)
|
|
21
19
|
padding 5px
|
|
22
20
|
border-radius 3px
|
|
23
21
|
pre
|
|
@@ -14,8 +14,8 @@ export default function CustomCss (props) {
|
|
|
14
14
|
const delta = useDelta(customCss)
|
|
15
15
|
|
|
16
16
|
async function applyTheme () {
|
|
17
|
-
const
|
|
18
|
-
|
|
17
|
+
const style = document.getElementById(themeDomId)
|
|
18
|
+
style.innerHTML = customCss
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
useEffect(() => {
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Standalone Input/TextArea Context Menu Module
|
|
3
|
+
* Automatically adds context menus to all input/textarea elements without code modification
|
|
4
|
+
* Just import this module and render <InputContextMenu /> in your React app to activate
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import React, { useState, useEffect, useRef } from 'react'
|
|
8
|
+
import { Dropdown } from 'antd'
|
|
9
|
+
import iconsMap from '../sys-menu/icons-map.jsx'
|
|
10
|
+
import { copy, readClipboard, readClipboardAsync } from '../../common/clipboard.js'
|
|
11
|
+
|
|
12
|
+
const e = window.translate
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Check if element is input or textarea
|
|
16
|
+
*/
|
|
17
|
+
function isInputElement (element) {
|
|
18
|
+
if (!element) return false
|
|
19
|
+
const tagName = element.tagName.toLowerCase()
|
|
20
|
+
return tagName === 'input' || tagName === 'textarea'
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Get input element state
|
|
25
|
+
*/
|
|
26
|
+
function getInputState (element) {
|
|
27
|
+
if (!isInputElement(element)) return null
|
|
28
|
+
|
|
29
|
+
const hasText = element.value && element.value.length > 0
|
|
30
|
+
const hasSelection = element.selectionStart !== element.selectionEnd
|
|
31
|
+
const isReadOnly = element.readOnly || element.disabled
|
|
32
|
+
|
|
33
|
+
let hasClipboard = false
|
|
34
|
+
try {
|
|
35
|
+
const content = readClipboard()
|
|
36
|
+
hasClipboard = content && content.length > 0
|
|
37
|
+
} catch (error) {
|
|
38
|
+
hasClipboard = false
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
hasText,
|
|
43
|
+
hasSelection,
|
|
44
|
+
hasClipboard,
|
|
45
|
+
isReadOnly
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Handle copy operation
|
|
51
|
+
*/
|
|
52
|
+
function handleCopy (element) {
|
|
53
|
+
if (!isInputElement(element)) return
|
|
54
|
+
|
|
55
|
+
const selectedText = element.value.substring(element.selectionStart, element.selectionEnd)
|
|
56
|
+
if (selectedText) {
|
|
57
|
+
copy(selectedText)
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Handle paste operation
|
|
63
|
+
*/
|
|
64
|
+
async function handlePaste (element) {
|
|
65
|
+
if (!isInputElement(element) || element.readOnly || element.disabled) return
|
|
66
|
+
|
|
67
|
+
element.focus()
|
|
68
|
+
const clipboardText = await readClipboardAsync()
|
|
69
|
+
|
|
70
|
+
if (clipboardText) {
|
|
71
|
+
insertTextAtCursor(element, clipboardText)
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Insert text at cursor position in input/textarea
|
|
77
|
+
*/
|
|
78
|
+
function insertTextAtCursor (element, text) {
|
|
79
|
+
if (!isInputElement(element)) return
|
|
80
|
+
|
|
81
|
+
element.focus()
|
|
82
|
+
|
|
83
|
+
// Try modern approach first - using execCommand
|
|
84
|
+
if (document.execCommand && document.queryCommandSupported('insertText')) {
|
|
85
|
+
try {
|
|
86
|
+
element.setSelectionRange(element.selectionStart, element.selectionEnd)
|
|
87
|
+
const success = document.execCommand('insertText', false, text)
|
|
88
|
+
if (success) {
|
|
89
|
+
triggerInputEvents(element)
|
|
90
|
+
}
|
|
91
|
+
} catch (error) {
|
|
92
|
+
console.log('execCommand insertText failed:', error)
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Trigger input events for React and other frameworks
|
|
99
|
+
*/
|
|
100
|
+
function triggerInputEvents (element) {
|
|
101
|
+
const inputEvent = new Event('input', { bubbles: true, cancelable: true })
|
|
102
|
+
element.dispatchEvent(inputEvent)
|
|
103
|
+
|
|
104
|
+
const changeEvent = new Event('change', { bubbles: true, cancelable: true })
|
|
105
|
+
element.dispatchEvent(changeEvent)
|
|
106
|
+
|
|
107
|
+
// Handle React's internal event handlers
|
|
108
|
+
const reactHandlers = Object.keys(element).find(key => key.startsWith('__reactProps') || key.startsWith('__reactEventHandlers'))
|
|
109
|
+
if (reactHandlers) {
|
|
110
|
+
const handler = element[reactHandlers]
|
|
111
|
+
if (handler?.onChange) {
|
|
112
|
+
handler.onChange({ target: element, currentTarget: element })
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Handle cut operation
|
|
119
|
+
*/
|
|
120
|
+
function handleCut (element) {
|
|
121
|
+
if (!isInputElement(element) || element.readOnly || element.disabled) return
|
|
122
|
+
|
|
123
|
+
const { selectionStart, selectionEnd } = element
|
|
124
|
+
const selectedText = element.value.substring(selectionStart, selectionEnd)
|
|
125
|
+
|
|
126
|
+
if (selectedText) {
|
|
127
|
+
element.focus()
|
|
128
|
+
element.setSelectionRange(selectionStart, selectionEnd)
|
|
129
|
+
|
|
130
|
+
// Try using execCommand for cut
|
|
131
|
+
if (document.execCommand && document.queryCommandSupported('cut')) {
|
|
132
|
+
try {
|
|
133
|
+
const success = document.execCommand('cut')
|
|
134
|
+
if (success) {
|
|
135
|
+
triggerInputEvents(element)
|
|
136
|
+
}
|
|
137
|
+
} catch (error) {
|
|
138
|
+
console.log('execCommand cut failed:', error)
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Handle select all operation
|
|
146
|
+
*/
|
|
147
|
+
function handleSelectAll (element) {
|
|
148
|
+
if (!isInputElement(element)) return
|
|
149
|
+
element.select()
|
|
150
|
+
element.focus()
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Create context menu items based on input state
|
|
155
|
+
*/
|
|
156
|
+
function createContextMenuItems (element) {
|
|
157
|
+
const state = getInputState(element)
|
|
158
|
+
if (!state) return []
|
|
159
|
+
|
|
160
|
+
const { hasText, hasSelection, hasClipboard, isReadOnly } = state
|
|
161
|
+
|
|
162
|
+
return [
|
|
163
|
+
{
|
|
164
|
+
key: 'copy',
|
|
165
|
+
icon: 'CopyOutlined',
|
|
166
|
+
label: e('copy'),
|
|
167
|
+
disabled: !hasSelection
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
key: 'cut',
|
|
171
|
+
icon: 'FileExcelOutlined',
|
|
172
|
+
label: e('cut'),
|
|
173
|
+
disabled: !hasSelection || isReadOnly
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
key: 'paste',
|
|
177
|
+
icon: 'SwitcherOutlined',
|
|
178
|
+
label: e('paste'),
|
|
179
|
+
disabled: !hasClipboard || isReadOnly
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
key: 'selectAll',
|
|
183
|
+
icon: 'CheckSquareOutlined',
|
|
184
|
+
label: e('selectall'),
|
|
185
|
+
disabled: !hasText
|
|
186
|
+
}
|
|
187
|
+
]
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* React Component for Input Context Menu
|
|
192
|
+
* Mount this component in your app to enable context menus on all input/textarea elements
|
|
193
|
+
*/
|
|
194
|
+
const InputContextMenu = () => {
|
|
195
|
+
const [visible, setVisible] = useState(false)
|
|
196
|
+
const [position, setPosition] = useState({ x: 0, y: 0 })
|
|
197
|
+
const [targetElement, setTargetElement] = useState(null)
|
|
198
|
+
const menuRef = useRef(null)
|
|
199
|
+
|
|
200
|
+
const handleMenuClick = async ({ key }) => {
|
|
201
|
+
// Keep reference to current element before closing menu
|
|
202
|
+
const currentElement = targetElement
|
|
203
|
+
// Close menu first
|
|
204
|
+
setVisible(false)
|
|
205
|
+
setTargetElement(null)
|
|
206
|
+
// Execute action with preserved element reference
|
|
207
|
+
if (currentElement) {
|
|
208
|
+
setTimeout(async () => {
|
|
209
|
+
try {
|
|
210
|
+
switch (key) {
|
|
211
|
+
case 'copy':
|
|
212
|
+
handleCopy(currentElement)
|
|
213
|
+
break
|
|
214
|
+
case 'cut':
|
|
215
|
+
handleCut(currentElement)
|
|
216
|
+
break
|
|
217
|
+
case 'paste':
|
|
218
|
+
await handlePaste(currentElement)
|
|
219
|
+
break
|
|
220
|
+
case 'selectAll':
|
|
221
|
+
handleSelectAll(currentElement)
|
|
222
|
+
break
|
|
223
|
+
default:
|
|
224
|
+
console.warn('Unknown menu action:', key)
|
|
225
|
+
}
|
|
226
|
+
} catch (error) {
|
|
227
|
+
console.error('Menu action failed:', error)
|
|
228
|
+
}
|
|
229
|
+
}, 10) // Small delay to ensure menu is closed
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
useEffect(() => {
|
|
234
|
+
const handleContextMenu = (event) => {
|
|
235
|
+
const target = event.target
|
|
236
|
+
if (isInputElement(target)) {
|
|
237
|
+
event.preventDefault()
|
|
238
|
+
event.stopPropagation()
|
|
239
|
+
setPosition({ x: event.clientX, y: event.clientY })
|
|
240
|
+
setTargetElement(target)
|
|
241
|
+
setVisible(true)
|
|
242
|
+
} else {
|
|
243
|
+
setVisible(false)
|
|
244
|
+
setTargetElement(null)
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const handleClick = (event) => {
|
|
249
|
+
if (visible && menuRef.current && !menuRef.current.contains(event.target)) {
|
|
250
|
+
setVisible(false)
|
|
251
|
+
setTargetElement(null)
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
document.addEventListener('contextmenu', handleContextMenu, true)
|
|
256
|
+
document.addEventListener('click', handleClick, true)
|
|
257
|
+
return () => {
|
|
258
|
+
document.removeEventListener('contextmenu', handleContextMenu, true)
|
|
259
|
+
document.removeEventListener('click', handleClick, true)
|
|
260
|
+
}
|
|
261
|
+
}, [visible])
|
|
262
|
+
const items = createContextMenuItems(targetElement) || []
|
|
263
|
+
const menuItems = items.map(item => {
|
|
264
|
+
const IconComponent = item.icon && iconsMap[item.icon]
|
|
265
|
+
return {
|
|
266
|
+
...item,
|
|
267
|
+
icon: IconComponent ? <IconComponent /> : null
|
|
268
|
+
}
|
|
269
|
+
})
|
|
270
|
+
return (
|
|
271
|
+
<div
|
|
272
|
+
ref={menuRef}
|
|
273
|
+
style={{
|
|
274
|
+
position: 'fixed',
|
|
275
|
+
left: position.x,
|
|
276
|
+
top: position.y,
|
|
277
|
+
zIndex: 9999
|
|
278
|
+
}}
|
|
279
|
+
>
|
|
280
|
+
<Dropdown
|
|
281
|
+
menu={{
|
|
282
|
+
items: menuItems,
|
|
283
|
+
onClick: handleMenuClick
|
|
284
|
+
}}
|
|
285
|
+
open={visible}
|
|
286
|
+
placement='bottomLeft'
|
|
287
|
+
>
|
|
288
|
+
<div style={{ width: 1, height: 1 }} />
|
|
289
|
+
</Dropdown>
|
|
290
|
+
</div>
|
|
291
|
+
)
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
export default InputContextMenu
|
|
@@ -1,11 +1,10 @@
|
|
|
1
|
-
@require '../../css/includes/theme-default'
|
|
2
1
|
.transports-wrap
|
|
3
2
|
position absolute
|
|
4
3
|
left 60px
|
|
5
4
|
top 3px
|
|
6
5
|
right 60px
|
|
7
6
|
height 36px
|
|
8
|
-
|
|
7
|
+
|
|
9
8
|
.transports-circle-wrap
|
|
10
9
|
text-align center
|
|
11
10
|
position relative
|
|
@@ -16,7 +15,7 @@
|
|
|
16
15
|
right 0
|
|
17
16
|
overflow-y scroll
|
|
18
17
|
z-index 11
|
|
19
|
-
background main
|
|
18
|
+
background var(--main)
|
|
20
19
|
|
|
21
20
|
.transports-wrap
|
|
22
21
|
.transports-dd
|
|
@@ -25,7 +24,7 @@
|
|
|
25
24
|
display block
|
|
26
25
|
|
|
27
26
|
.transports-title
|
|
28
|
-
border-bottom 1px solid primary
|
|
27
|
+
border-bottom 1px solid var(--primary)
|
|
29
28
|
padding 10px 15px
|
|
30
29
|
.transports-content
|
|
31
30
|
max-height 200px
|
|
@@ -34,11 +33,9 @@
|
|
|
34
33
|
display flex
|
|
35
34
|
padding 8px 5px
|
|
36
35
|
&:hover
|
|
37
|
-
background main-
|
|
36
|
+
background var(--main-darker)
|
|
38
37
|
.sftp-transport .transfer-control-icon:hover
|
|
39
|
-
color primary
|
|
40
|
-
.transports-count
|
|
41
|
-
color text
|
|
38
|
+
color var(--primary)
|
|
42
39
|
|
|
43
40
|
.flex-child + .flex-child
|
|
44
41
|
margin-left 5px
|
|
@@ -1,13 +1,10 @@
|
|
|
1
|
-
@require '../../css/includes/theme-default'
|
|
2
|
-
|
|
3
1
|
.main-footer
|
|
4
|
-
background main
|
|
2
|
+
background var(--main)
|
|
5
3
|
height 36px
|
|
6
4
|
position absolute
|
|
7
5
|
left 44px
|
|
8
6
|
right 0
|
|
9
7
|
bottom 0
|
|
10
|
-
color text
|
|
11
8
|
z-index 200
|
|
12
9
|
.pinned
|
|
13
10
|
.main-footer
|
|
@@ -52,4 +49,4 @@
|
|
|
52
49
|
right 0
|
|
53
50
|
bottom 0
|
|
54
51
|
z-index 100
|
|
55
|
-
background main
|
|
52
|
+
background var(--main)
|
|
@@ -20,9 +20,9 @@ const troubleshootContent = {
|
|
|
20
20
|
windows: 'path\\to\\electerm.exe'
|
|
21
21
|
},
|
|
22
22
|
clearConfig: {
|
|
23
|
-
mac: 'rm -rf ~/Library/Application\\ Support/electerm/users/default_user/electerm.data.nedb',
|
|
24
|
-
linux: 'rm -rf ~/.config/electerm/users/default_user/electerm.data.nedb',
|
|
25
|
-
windows: 'Delete C:\\Users\\your-user-name\\AppData\\Roaming\\electerm\\users\\default_user\\electerm.data.nedb'
|
|
23
|
+
mac: 'rm -rf ~/Library/Application\\ Support/electerm/users/default_user/electerm_data.db && rm -rf ~/Library/Application\\ Support/electerm/users/default_user/electerm.data.nedb',
|
|
24
|
+
linux: 'rm -rf ~/.config/electerm/users/default_user/electerm_data.db && rm -rf ~/.config/electerm/users/default_user/electerm.data.nedb',
|
|
25
|
+
windows: 'Delete C:\\Users\\your-user-name\\AppData\\Roaming\\electerm\\users\\default_user\\electerm_data.db && Delete C:\\Users\\your-user-name\\AppData\\Roaming\\electerm\\users\\default_user\\electerm.data.nedb'
|
|
26
26
|
},
|
|
27
27
|
clearData: {
|
|
28
28
|
mac: 'rm -rf ~/Library/Application\\ Support/electerm*',
|
|
@@ -29,6 +29,7 @@ import SshConfigLoadNotify from '../ssh-config/ssh-config-load-notify'
|
|
|
29
29
|
import LoadSshConfigs from '../ssh-config/load-ssh-configs'
|
|
30
30
|
import AIChat from '../ai/ai-chat'
|
|
31
31
|
import Opacity from '../common/opacity'
|
|
32
|
+
import InputContextMenu from '../common/input-context-menu'
|
|
32
33
|
import { pick } from 'lodash-es'
|
|
33
34
|
import deepCopy from 'json-deep-copy'
|
|
34
35
|
import './wrapper.styl'
|
|
@@ -248,6 +249,7 @@ export default auto(function Index (props) {
|
|
|
248
249
|
theme={uiThemeConfig}
|
|
249
250
|
>
|
|
250
251
|
<div {...ext1}>
|
|
252
|
+
<InputContextMenu />
|
|
251
253
|
<ShortcutControl config={config} />
|
|
252
254
|
<TermFullscreenControl
|
|
253
255
|
terminalFullScreen={terminalFullScreen}
|
|
@@ -260,7 +262,6 @@ export default auto(function Index (props) {
|
|
|
260
262
|
<TerminalInteractive />
|
|
261
263
|
<UiTheme
|
|
262
264
|
{...themeProps}
|
|
263
|
-
buildTheme={store.buildTheme}
|
|
264
265
|
/>
|
|
265
266
|
<CustomCss customCss={config.customCss} />
|
|
266
267
|
<TextEditor />
|
|
@@ -5,20 +5,60 @@
|
|
|
5
5
|
import { useEffect } from 'react'
|
|
6
6
|
import { useDelta, useConditionalEffect } from 'react-delta-hooks'
|
|
7
7
|
import eq from 'fast-deep-equal'
|
|
8
|
+
import isColorDark from '../../common/is-color-dark'
|
|
8
9
|
|
|
9
10
|
const themeDomId = 'theme-css'
|
|
10
11
|
|
|
12
|
+
function darker (color, amount = 0.1) {
|
|
13
|
+
let usePound = false
|
|
14
|
+
|
|
15
|
+
if (color[0] === '#') {
|
|
16
|
+
color = color.slice(1)
|
|
17
|
+
usePound = true
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const num = parseInt(color, 16)
|
|
21
|
+
|
|
22
|
+
let r = (num >> 16) - Math.round(255 * amount)
|
|
23
|
+
if (r < 0) r = 0
|
|
24
|
+
let b = ((num >> 8) & 0x00FF) - Math.round(255 * amount)
|
|
25
|
+
if (b < 0) b = 0
|
|
26
|
+
let g = (num & 0x0000FF) - Math.round(255 * amount)
|
|
27
|
+
if (g < 0) g = 0
|
|
28
|
+
|
|
29
|
+
return (usePound ? '#' : '') + (g | (b << 8) | (r << 16)).toString(16)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function buildTheme (themeConfig) {
|
|
33
|
+
const keys = Object.keys(themeConfig || {})
|
|
34
|
+
const themeCss = keys.map(key => {
|
|
35
|
+
const val = themeConfig[key]
|
|
36
|
+
if (key === 'primary') {
|
|
37
|
+
const contrast = isColorDark(val) ? '#fff' : '#000'
|
|
38
|
+
return `--${key}-contrast: ${contrast};\n--${key}: ${val};`
|
|
39
|
+
} else if (key === 'main') {
|
|
40
|
+
const darkerMain = darker(val, 0.3)
|
|
41
|
+
const lighterMain = darker(val, -0.3)
|
|
42
|
+
return `--${key}-darker: ${darkerMain};\n--${key}-lighter: ${lighterMain};\n--${key}: ${val};`
|
|
43
|
+
}
|
|
44
|
+
return `--${key}: ${val};`
|
|
45
|
+
}).join('\n')
|
|
46
|
+
if (themeCss) {
|
|
47
|
+
const css = `:root {\n${themeCss}\n}\n`
|
|
48
|
+
return Promise.resolve(css)
|
|
49
|
+
}
|
|
50
|
+
return Promise.resolve('')
|
|
51
|
+
}
|
|
52
|
+
|
|
11
53
|
export default function UiTheme (props) {
|
|
12
|
-
const { themeConfig
|
|
54
|
+
const { themeConfig } = props
|
|
13
55
|
|
|
14
56
|
const delta = useDelta(themeConfig)
|
|
15
57
|
|
|
16
58
|
async function applyTheme () {
|
|
17
|
-
const
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
} = await buildTheme(themeConfig)
|
|
21
|
-
stylus.innerHTML = stylusCss
|
|
59
|
+
const style = document.getElementById(themeDomId)
|
|
60
|
+
const css = await buildTheme(themeConfig)
|
|
61
|
+
style.innerHTML = css
|
|
22
62
|
}
|
|
23
63
|
|
|
24
64
|
useEffect(() => {
|
|
@@ -1,13 +1,11 @@
|
|
|
1
|
-
@require '../../css/includes/theme-default'
|
|
2
1
|
.upgrade-panel
|
|
3
2
|
position fixed
|
|
4
3
|
right 10px
|
|
5
4
|
bottom 10px
|
|
6
5
|
z-index 9999
|
|
7
|
-
background main
|
|
8
|
-
color text
|
|
6
|
+
background var(--main)
|
|
9
7
|
border-radius 5px
|
|
10
|
-
border 1px solid
|
|
8
|
+
border 1px solid var(--main-darker)
|
|
11
9
|
padding 0
|
|
12
10
|
width 440px
|
|
13
11
|
.upgrade-panel-hide
|
|
@@ -17,12 +15,12 @@
|
|
|
17
15
|
bottom auto
|
|
18
16
|
.close-upgrade-panel
|
|
19
17
|
&:hover
|
|
20
|
-
color success
|
|
18
|
+
color var(--success)
|
|
21
19
|
.upgrade-panel-title
|
|
22
20
|
.upgrade-panel-body
|
|
23
21
|
padding 10px 15px
|
|
24
22
|
.upgrade-panel-title
|
|
25
|
-
border-bottom 1px solid
|
|
23
|
+
border-bottom 1px solid var(--main-darker)
|
|
26
24
|
.markdown-wrap
|
|
27
25
|
max-height 40vh
|
|
28
26
|
overflow-y auto
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
@require '../../css/includes/theme-default'
|
|
2
1
|
.common-err-desc
|
|
3
2
|
&:hover
|
|
4
3
|
text-overflow clip
|
|
@@ -6,43 +5,22 @@
|
|
|
6
5
|
white-space pre
|
|
7
6
|
max-height 300px
|
|
8
7
|
overflow scroll
|
|
9
|
-
|
|
10
|
-
// margin-left 343px
|
|
8
|
+
|
|
11
9
|
.error-wrapper
|
|
12
|
-
background main
|
|
10
|
+
background var(--main)
|
|
13
11
|
height 100%
|
|
14
12
|
position fixed
|
|
15
|
-
color text
|
|
16
13
|
overflow-y scroll
|
|
17
|
-
.init-wrap
|
|
18
|
-
.loading-data
|
|
19
|
-
position fixed
|
|
20
|
-
left 0
|
|
21
|
-
top 0
|
|
22
|
-
width 100%
|
|
23
|
-
height 100%
|
|
24
|
-
background-color main-light
|
|
25
|
-
.loading-data
|
|
26
|
-
text-align center
|
|
27
|
-
font-size 30px
|
|
28
|
-
color text
|
|
29
|
-
padding 50px
|
|
30
|
-
z-index 210
|
|
31
|
-
.ant-tag
|
|
32
|
-
border-radius 3px
|
|
33
|
-
padding 4px
|
|
34
|
-
font-size 14px
|
|
35
|
-
.loaded
|
|
36
|
-
.loading-data
|
|
37
|
-
display none
|
|
38
14
|
|
|
39
15
|
#container
|
|
16
|
+
.xterm
|
|
17
|
+
padding 10px
|
|
40
18
|
.session-batch-active
|
|
41
19
|
.xterm-screen
|
|
42
20
|
&::before
|
|
43
21
|
font-size 30vmin
|
|
44
22
|
font-weight bold
|
|
45
|
-
color
|
|
23
|
+
color var(--text)
|
|
46
24
|
display flex
|
|
47
25
|
justify-content center
|
|
48
26
|
align-items center
|
|
@@ -1,13 +1,11 @@
|
|
|
1
|
-
@require '../../css/includes/theme-default'
|
|
2
1
|
.qm-list-wrap
|
|
3
2
|
max-height calc(100vh - 100px)
|
|
4
3
|
overflow-y scroll
|
|
5
4
|
.qm-wrap-tooltip
|
|
6
|
-
background main
|
|
5
|
+
background var(--main)
|
|
7
6
|
position absolute
|
|
8
7
|
left 43px
|
|
9
8
|
right 0
|
|
10
|
-
color text
|
|
11
9
|
z-index 89
|
|
12
10
|
height auto
|
|
13
11
|
bottom 36px
|
|
@@ -21,7 +19,7 @@
|
|
|
21
19
|
.fil-keyword
|
|
22
20
|
.fil-label
|
|
23
21
|
.qm-item
|
|
24
|
-
color
|
|
22
|
+
color var(--text-light)
|
|
25
23
|
.qm-item
|
|
26
24
|
&.name-match
|
|
27
25
|
&.label-match
|
|
@@ -31,4 +29,4 @@
|
|
|
31
29
|
width 100px
|
|
32
30
|
|
|
33
31
|
.item-list-unit.dragover
|
|
34
|
-
border: 1px dashed primary
|
|
32
|
+
border: 1px dashed var(--primary)
|