@electerm/electerm-react 5.5.15 → 5.5.35
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/README.md +14 -4
- package/client/common/bookmark-schemas.js +3 -0
- package/client/common/constants.js +16 -0
- package/client/common/create-title.jsx +5 -1
- package/client/common/default-setting.js +4 -2
- package/client/common/parse-vv.js +407 -0
- package/client/common/theme-defaults.js +5 -2
- package/client/common/update-check.js +5 -4
- package/client/common/vv-to-tab.js +120 -0
- package/client/components/ai/agent-tool-call-card.jsx +19 -4
- package/client/components/ai/agent.js +33 -16
- package/client/components/ai/ai-chat-history-item.jsx +12 -48
- package/client/components/ai/ai-chat.jsx +82 -25
- package/client/components/ai/ai-config-props.js +12 -1
- package/client/components/ai/ai-config.jsx +14 -0
- package/client/components/ai/ai-context-indicator.jsx +83 -0
- package/client/components/ai/ai-context.js +272 -0
- package/client/components/ai/ai-output.jsx +4 -0
- package/client/components/ai/ai-presets.js +13 -2
- package/client/components/ai/ai.styl +105 -6
- package/client/components/ai/copy-icon.jsx +31 -0
- package/client/components/bg/css-overwrite.jsx +16 -1
- package/client/components/bookmark-form/common/fields.jsx +3 -0
- package/client/components/bookmark-form/common/vv-file-field.jsx +138 -0
- package/client/components/bookmark-form/config/spice.js +22 -0
- package/client/components/footer/cmd-history.jsx +279 -65
- package/client/components/footer/cmd-history.styl +68 -1
- package/client/components/footer/footer-entry.jsx +46 -15
- package/client/components/icons/ai-icon.styl +4 -1
- package/client/components/layout/layout.jsx +12 -13
- package/client/components/main/main.jsx +23 -3
- package/client/components/main/term-fullscreen.styl +7 -0
- package/client/components/quick-commands/qm.styl +58 -0
- package/client/components/quick-commands/quick-command-ai-editor.jsx +255 -0
- package/client/components/quick-commands/quick-command-ai-editor.styl +13 -0
- package/client/components/quick-commands/quick-commands-box.jsx +197 -82
- package/client/components/quick-commands/quick-commands-form-elem.jsx +129 -65
- package/client/components/quick-commands/quick-commands-select.jsx +11 -0
- package/client/components/setting-panel/list.jsx +2 -2
- package/client/components/setting-panel/setting-common.jsx +2 -0
- package/client/components/side-panel-r/right-side-panel.styl +11 -1
- package/client/components/side-panel-r/side-panel-r.jsx +56 -13
- package/client/components/sidebar/bookmark-select.jsx +17 -3
- package/client/components/sidebar/history-item.jsx +20 -4
- package/client/components/sidebar/history.jsx +7 -0
- package/client/components/sidebar/index.jsx +13 -4
- package/client/components/sidebar/side-panel.jsx +8 -3
- package/client/components/sidebar/sidebar-panel.jsx +20 -7
- package/client/components/terminal/shortcut-bar.styl +14 -0
- package/client/components/terminal/unix-timestamp-tooltip.jsx +27 -1
- package/client/components/terminal-info/terminal-info.styl +3 -0
- package/client/components/theme/terminal-theme-list.styl +3 -0
- package/client/components/theme/theme-ai-editor.jsx +2 -2
- package/client/components/theme/theme-form.jsx +2 -2
- package/client/components/theme/theme-list-item.jsx +59 -25
- package/client/components/tree-list/tree-list.jsx +31 -3
- package/client/css/mobile.styl +28 -0
- package/client/store/common.js +164 -32
- package/client/store/init-state.js +10 -1
- package/client/store/load-data.js +72 -0
- package/client/store/sidebar.js +5 -1
- package/client/store/store.js +15 -2
- package/client/store/watch.js +3 -1
- package/package.json +1 -1
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
import React, { memo, useRef } from 'react'
|
|
1
|
+
import React, { memo, useRef, useEffect } from 'react'
|
|
2
2
|
import DragHandle from '../common/drag-handle'
|
|
3
3
|
import './right-side-panel.styl'
|
|
4
4
|
import {
|
|
5
5
|
CloseCircleOutlined,
|
|
6
6
|
PushpinOutlined,
|
|
7
|
-
BarChartOutlined
|
|
7
|
+
BarChartOutlined,
|
|
8
|
+
HistoryOutlined,
|
|
9
|
+
ThunderboltOutlined
|
|
8
10
|
} from '@ant-design/icons'
|
|
9
11
|
import {
|
|
10
12
|
Typography,
|
|
@@ -19,17 +21,52 @@ export default memo(function RightSidePanel (
|
|
|
19
21
|
rightPanelWidth,
|
|
20
22
|
children,
|
|
21
23
|
title,
|
|
22
|
-
rightPanelTab
|
|
24
|
+
rightPanelTab,
|
|
25
|
+
isMobile
|
|
23
26
|
}
|
|
24
27
|
) {
|
|
25
28
|
const panelRef = useRef(null)
|
|
26
29
|
|
|
30
|
+
// Esc closes the panel, but never at the terminal's expense: xterm forwards
|
|
31
|
+
// Esc to the remote shell, so stealing it there would break vim, less, etc.
|
|
32
|
+
// The guard checks the event target rather than focus state, because the
|
|
33
|
+
// keydown bubbles up from xterm's textarea inside .term-wrap — the same
|
|
34
|
+
// selector shortcut-bar-entry.jsx uses to detect terminal focus. A modal owns
|
|
35
|
+
// Esc while one is open. This hook has to stay above the early return below
|
|
36
|
+
// so the hook order never changes.
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
if (!rightPanelVisible) {
|
|
39
|
+
return
|
|
40
|
+
}
|
|
41
|
+
function onKeyDown (event) {
|
|
42
|
+
if (event.key !== 'Escape' || window.store.showModal) {
|
|
43
|
+
return
|
|
44
|
+
}
|
|
45
|
+
const { target } = event
|
|
46
|
+
if (target && typeof target.closest === 'function' && target.closest('.term-wrap')) {
|
|
47
|
+
return
|
|
48
|
+
}
|
|
49
|
+
window.store.rightPanelVisible = false
|
|
50
|
+
}
|
|
51
|
+
document.addEventListener('keydown', onKeyDown)
|
|
52
|
+
return () => document.removeEventListener('keydown', onKeyDown)
|
|
53
|
+
}, [rightPanelVisible])
|
|
54
|
+
|
|
27
55
|
if (!rightPanelVisible) {
|
|
28
56
|
return null
|
|
29
57
|
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
58
|
+
// Mobile has no pin control (it is not rendered below), so a pin persisted
|
|
59
|
+
// from a desktop session must not turn the panel into a dock here — that
|
|
60
|
+
// would make it full height over the footer for no reachable reason.
|
|
61
|
+
const pinned = rightPanelPinned && !isMobile
|
|
62
|
+
// one glyph per tab, so the panel says what it is showing; the info tab is
|
|
63
|
+
// the fallback, since a stale or unknown tab falls back to it everywhere else
|
|
64
|
+
const tagMap = {
|
|
65
|
+
ai: <Tag className='mg1r'>AI</Tag>,
|
|
66
|
+
cmdHistory: <HistoryOutlined className='mg1r' />,
|
|
67
|
+
quickCommands: <ThunderboltOutlined className='mg1r' />
|
|
68
|
+
}
|
|
69
|
+
const tag = tagMap[rightPanelTab] || <BarChartOutlined className='mg1r' />
|
|
33
70
|
|
|
34
71
|
function onDragEnd (nw) {
|
|
35
72
|
window.store.setRightSidePanelWidth(nw)
|
|
@@ -46,11 +83,11 @@ export default memo(function RightSidePanel (
|
|
|
46
83
|
}
|
|
47
84
|
|
|
48
85
|
function togglePin () {
|
|
49
|
-
window.store.
|
|
86
|
+
window.store.setRightPanelPinned(!window.store.rightPanelPinned)
|
|
50
87
|
}
|
|
51
88
|
|
|
52
89
|
const panelProps = {
|
|
53
|
-
className: 'right-side-panel animate-fast' + (
|
|
90
|
+
className: 'right-side-panel animate-fast' + (pinned ? ' right-side-panel-pinned' : ''),
|
|
54
91
|
ref: panelRef,
|
|
55
92
|
style: {
|
|
56
93
|
width: `${rightPanelWidth}px`
|
|
@@ -58,7 +95,7 @@ export default memo(function RightSidePanel (
|
|
|
58
95
|
}
|
|
59
96
|
|
|
60
97
|
const pinProps = {
|
|
61
|
-
className: 'right-side-panel-pin right-side-panel-controls' + (
|
|
98
|
+
className: 'right-side-panel-pin right-side-panel-controls' + (pinned ? ' pinned' : ''),
|
|
62
99
|
onClick: togglePin
|
|
63
100
|
}
|
|
64
101
|
const dragProps = {
|
|
@@ -73,7 +110,11 @@ export default memo(function RightSidePanel (
|
|
|
73
110
|
<div
|
|
74
111
|
{...panelProps}
|
|
75
112
|
>
|
|
76
|
-
|
|
113
|
+
{/* Mobile panels are full-screen drawers (see store.rightPanelWidth):
|
|
114
|
+
their width is the viewport and is not adjustable, so the resize
|
|
115
|
+
handle is not rendered — it would otherwise sit on the screen's left
|
|
116
|
+
edge and drag a size the panel never uses. */}
|
|
117
|
+
{!isMobile && <DragHandle {...dragProps} />}
|
|
77
118
|
<Flex
|
|
78
119
|
className='right-panel-title pd2'
|
|
79
120
|
justify='space-between'
|
|
@@ -83,9 +124,11 @@ export default memo(function RightSidePanel (
|
|
|
83
124
|
{tag} {title}
|
|
84
125
|
</Typography.Text>
|
|
85
126
|
<Flex>
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
127
|
+
{!isMobile && (
|
|
128
|
+
<PushpinOutlined
|
|
129
|
+
{...pinProps}
|
|
130
|
+
/>
|
|
131
|
+
)}
|
|
89
132
|
<CloseCircleOutlined
|
|
90
133
|
className='right-side-panel-close right-side-panel-controls mg1l'
|
|
91
134
|
onClick={onClose}
|
|
@@ -2,11 +2,13 @@
|
|
|
2
2
|
* bookmark select
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
import { useState } from 'react'
|
|
5
6
|
import { auto } from 'manate/react'
|
|
6
7
|
import TreeList from '../tree-list/tree-list'
|
|
7
8
|
|
|
8
9
|
export default auto(function BookmarkSelect (props) {
|
|
9
10
|
const { store, from, autoFocus } = props
|
|
11
|
+
const [activeItemId, setActiveItemId] = useState('')
|
|
10
12
|
const {
|
|
11
13
|
listStyle,
|
|
12
14
|
openedSideBar,
|
|
@@ -19,16 +21,28 @@ export default auto(function BookmarkSelect (props) {
|
|
|
19
21
|
if (from === 'sidebar' && openedSideBar !== 'bookmarks') {
|
|
20
22
|
return null
|
|
21
23
|
}
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
+
const openBookmark = (item) => {
|
|
25
|
+
// A pinned (docked) panel stays open; anything else — including mobile,
|
|
26
|
+
// where pinning is not a reachable mode — dismisses on selection.
|
|
27
|
+
if (!(store.pinned && !store.isMobile)) {
|
|
24
28
|
store.setOpenedSideBar('')
|
|
25
29
|
}
|
|
26
30
|
store.onSelectBookmark(item.id)
|
|
27
31
|
}
|
|
32
|
+
// doubleClickToOpenBookmark: a single click only marks the row, the session
|
|
33
|
+
// opens on a double click. Touch devices have no double click, so the setting
|
|
34
|
+
// is ignored there. This is the sidebar list only — the settings bookmarks tab
|
|
35
|
+
// keeps selecting for editing on a single click.
|
|
36
|
+
const openOnDoubleClick = !!store.config.doubleClickToOpenBookmark &&
|
|
37
|
+
!store.isTouchDevice
|
|
28
38
|
const base = {
|
|
29
39
|
bookmarks: bookmarks || [],
|
|
30
40
|
type: 'bookmarks',
|
|
31
|
-
|
|
41
|
+
activeItemId: openOnDoubleClick ? activeItemId : undefined,
|
|
42
|
+
onClickItem: openOnDoubleClick
|
|
43
|
+
? item => setActiveItemId(item.id)
|
|
44
|
+
: openBookmark,
|
|
45
|
+
onDoubleClickItem: openOnDoubleClick ? openBookmark : undefined,
|
|
32
46
|
listStyle,
|
|
33
47
|
staticList: true
|
|
34
48
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import React, { useCallback, useEffect, useRef } from 'react'
|
|
1
|
+
import React, { useCallback, useEffect, useRef, useState } from 'react'
|
|
2
|
+
import classnames from 'classnames'
|
|
2
3
|
import createTitle, { createTitleWithTag } from '../../common/create-title'
|
|
3
4
|
import { DeleteOutlined, BookFilled } from '@ant-design/icons'
|
|
4
5
|
import { refsStatic } from '../common/ref'
|
|
@@ -6,18 +7,32 @@ import { refsStatic } from '../common/ref'
|
|
|
6
7
|
export default function HistoryItem (props) {
|
|
7
8
|
const { store } = window
|
|
8
9
|
const {
|
|
9
|
-
item
|
|
10
|
+
item,
|
|
11
|
+
openOnDoubleClick
|
|
10
12
|
} = props
|
|
13
|
+
const [active, setActive] = useState(false)
|
|
11
14
|
const timeoutRef = useRef(null)
|
|
12
15
|
|
|
13
16
|
const handleClick = useCallback(() => {
|
|
17
|
+
// with doubleClickToOpenBookmark on, a click only marks the row — the
|
|
18
|
+
// session opens on the double click below
|
|
19
|
+
if (openOnDoubleClick) {
|
|
20
|
+
setActive(true)
|
|
21
|
+
return
|
|
22
|
+
}
|
|
14
23
|
if (timeoutRef.current) {
|
|
15
24
|
clearTimeout(timeoutRef.current)
|
|
16
25
|
}
|
|
17
26
|
timeoutRef.current = setTimeout(() => {
|
|
18
27
|
store.onSelectHistory(item.tab)
|
|
19
28
|
}, 10)
|
|
20
|
-
}, [item.tab])
|
|
29
|
+
}, [item.tab, openOnDoubleClick])
|
|
30
|
+
|
|
31
|
+
const handleDoubleClick = useCallback(() => {
|
|
32
|
+
if (openOnDoubleClick) {
|
|
33
|
+
store.onSelectHistory(item.tab)
|
|
34
|
+
}
|
|
35
|
+
}, [item.tab, openOnDoubleClick])
|
|
21
36
|
|
|
22
37
|
useEffect(() => {
|
|
23
38
|
return () => {
|
|
@@ -47,9 +62,10 @@ export default function HistoryItem (props) {
|
|
|
47
62
|
const tt = createTitle(item.tab)
|
|
48
63
|
return (
|
|
49
64
|
<div
|
|
50
|
-
className='item-list-unit'
|
|
65
|
+
className={classnames('item-list-unit', { active })}
|
|
51
66
|
title={tt}
|
|
52
67
|
onClick={handleClick}
|
|
68
|
+
onDoubleClick={handleDoubleClick}
|
|
53
69
|
>
|
|
54
70
|
<div className='elli pd1y pd2x'>
|
|
55
71
|
{title}
|
|
@@ -25,6 +25,12 @@ export default auto(function HistoryPanel (props) {
|
|
|
25
25
|
const {
|
|
26
26
|
history
|
|
27
27
|
} = store
|
|
28
|
+
// doubleClickToOpenBookmark: a single click only marks the row, the session
|
|
29
|
+
// opens on a double click. Touch devices have no double click, so the setting
|
|
30
|
+
// is ignored there. Read here rather than in HistoryItem because this
|
|
31
|
+
// component is `auto` — it re-renders when the input modality flips.
|
|
32
|
+
const openOnDoubleClick = !!store.config.doubleClickToOpenBookmark &&
|
|
33
|
+
!store.isTouchDevice
|
|
28
34
|
let arr = store.config.disableConnectionHistory ? [] : history
|
|
29
35
|
if (sortByFrequency) {
|
|
30
36
|
arr = [...arr].sort((a, b) => { return b.count - a.count })
|
|
@@ -75,6 +81,7 @@ export default auto(function HistoryPanel (props) {
|
|
|
75
81
|
<HistoryItem
|
|
76
82
|
key={item.id}
|
|
77
83
|
item={item}
|
|
84
|
+
openOnDoubleClick={openOnDoubleClick}
|
|
78
85
|
/>
|
|
79
86
|
)
|
|
80
87
|
})
|
|
@@ -43,17 +43,24 @@ export default function Sidebar (props) {
|
|
|
43
43
|
openWidgetsModal,
|
|
44
44
|
zoom,
|
|
45
45
|
leftSideBarIcons,
|
|
46
|
-
widgetInstancesLength
|
|
46
|
+
widgetInstancesLength,
|
|
47
|
+
isMobile
|
|
47
48
|
} = props
|
|
48
49
|
|
|
49
50
|
const { store } = window
|
|
50
51
|
|
|
52
|
+
// pinned is desktop-only, like the right panel's: on mobile the panel spans
|
|
53
|
+
// the viewport and there is no pin control to unpin it, so a pin persisted
|
|
54
|
+
// from a desktop session must not turn into a mode the user cannot leave.
|
|
55
|
+
const pinnedActive = pinned && !isMobile
|
|
56
|
+
|
|
51
57
|
const handleClickOutside = (event) => {
|
|
52
58
|
const { store } = window
|
|
53
59
|
// Nothing to dismiss when the panel is closed or pinned; also ignore
|
|
54
60
|
// clicks while typing in an input so the panel is not yanked away
|
|
55
|
-
// mid-interaction.
|
|
56
|
-
|
|
61
|
+
// mid-interaction. Read the pin live rather than from the render scope:
|
|
62
|
+
// this listener is attached once, so a captured value would go stale.
|
|
63
|
+
if (!store.openedSideBar || (store.pinned && !store.isMobile) || hasActiveInput()) {
|
|
57
64
|
return
|
|
58
65
|
}
|
|
59
66
|
const target = event.target
|
|
@@ -221,10 +228,12 @@ export default function Sidebar (props) {
|
|
|
221
228
|
setLeftSidePanelWidth={setLeftSidePanelWidth}
|
|
222
229
|
leftSidePanelWidth={leftSidePanelWidth}
|
|
223
230
|
leftSideBarWidth={leftSideBarWidth}
|
|
231
|
+
isMobile={isMobile}
|
|
224
232
|
>
|
|
225
233
|
<SideBarPanel
|
|
226
|
-
pinned={
|
|
234
|
+
pinned={pinnedActive}
|
|
227
235
|
sidebarPanelTab={sidebarPanelTab}
|
|
236
|
+
isMobile={isMobile}
|
|
228
237
|
/>
|
|
229
238
|
</SidePanel>
|
|
230
239
|
</div>
|
|
@@ -26,15 +26,20 @@ export default function SidePanel (props) {
|
|
|
26
26
|
onDragMove,
|
|
27
27
|
left: true
|
|
28
28
|
}
|
|
29
|
+
// Mobile panels are full-screen drawers: the width is the viewport and is not
|
|
30
|
+
// adjustable, so the resize handle is not rendered at all — a handle there
|
|
31
|
+
// would sit on the screen edge and drag a size the panel does not use (see
|
|
32
|
+
// store.leftSidePanelWidth, which ignores the stored width on mobile).
|
|
33
|
+
const dragHandle = props.isMobile
|
|
34
|
+
? null
|
|
35
|
+
: <DragHandle {...dragProps} />
|
|
29
36
|
return (
|
|
30
37
|
<div
|
|
31
38
|
{...props.sideProps}
|
|
32
39
|
ref={panelRef}
|
|
33
40
|
draggable={false}
|
|
34
41
|
>
|
|
35
|
-
|
|
36
|
-
{...dragProps}
|
|
37
|
-
/>
|
|
42
|
+
{dragHandle}
|
|
38
43
|
{props.children}
|
|
39
44
|
</div>
|
|
40
45
|
)
|
|
@@ -20,7 +20,7 @@ import {
|
|
|
20
20
|
const e = window.translate
|
|
21
21
|
|
|
22
22
|
export default memo(function SidebarPanel (props) {
|
|
23
|
-
const { sidebarPanelTab, pinned } = props
|
|
23
|
+
const { sidebarPanelTab, pinned, isMobile } = props
|
|
24
24
|
const { store } = window
|
|
25
25
|
const [openSelectModal, setOpenSelectModal] = useState(false)
|
|
26
26
|
const prps = {
|
|
@@ -81,6 +81,22 @@ export default memo(function SidebarPanel (props) {
|
|
|
81
81
|
</Tooltip>
|
|
82
82
|
]
|
|
83
83
|
}
|
|
84
|
+
// No pin control on mobile: the panel spans the viewport there, so there is
|
|
85
|
+
// nothing to dock it beside, and the control row is tight enough that the
|
|
86
|
+
// pin would crowd the icons it shares it with.
|
|
87
|
+
function renderPinIcon () {
|
|
88
|
+
if (isMobile) {
|
|
89
|
+
return null
|
|
90
|
+
}
|
|
91
|
+
return (
|
|
92
|
+
<Tooltip title={e('pin')}>
|
|
93
|
+
<PushpinOutlined
|
|
94
|
+
{...prps1}
|
|
95
|
+
onClick={store.handlePin}
|
|
96
|
+
/>
|
|
97
|
+
</Tooltip>
|
|
98
|
+
)
|
|
99
|
+
}
|
|
84
100
|
return (
|
|
85
101
|
<div
|
|
86
102
|
className='sidebar-panel bookmarks-panel animate-fast'
|
|
@@ -100,12 +116,9 @@ export default memo(function SidebarPanel (props) {
|
|
|
100
116
|
{
|
|
101
117
|
renderExpandIcons()
|
|
102
118
|
}
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
onClick={store.handlePin}
|
|
107
|
-
/>
|
|
108
|
-
</Tooltip>
|
|
119
|
+
{
|
|
120
|
+
renderPinIcon()
|
|
121
|
+
}
|
|
109
122
|
<CloseOutlined
|
|
110
123
|
{...prps}
|
|
111
124
|
onClick={store.handleCloseSidebar}
|
|
@@ -81,6 +81,20 @@
|
|
|
81
81
|
bottom calc(var(--shortcut-bar-h, 44px) + var(--shortcut-bar-kb-offset, 0px))
|
|
82
82
|
.remote-monitor-bar
|
|
83
83
|
bottom calc(var(--shortcut-bar-h, 44px) + var(--shortcut-bar-kb-offset, 0px) + 36px)
|
|
84
|
+
// the right panel stacks on the footer exactly like the monitor bar, so it
|
|
85
|
+
// needs the same lift. Pinned is excluded: a pinned panel is a full-height
|
|
86
|
+
// dock and owns bottom 0. Mobile is excluded as well: there the panel is a
|
|
87
|
+
// full-screen drawer that covers the footer on purpose (mobile.styl).
|
|
88
|
+
&:not(.is-mobile)
|
|
89
|
+
.right-side-panel:not(.right-side-panel-pinned)
|
|
90
|
+
bottom calc(var(--shortcut-bar-h, 44px) + var(--shortcut-bar-kb-offset, 0px) + 36px)
|
|
91
|
+
// A pinned panel is still a dock, but it yields to the shortcut bar rather
|
|
92
|
+
// than the other way round: the bar is z-index 400 and spans the full
|
|
93
|
+
// viewport width by design (it ignores the left sidebar too), so it would
|
|
94
|
+
// otherwise paint over the dock's bottom edge. The dock stops above the bar
|
|
95
|
+
// only — the footer sits beside the dock, not below it.
|
|
96
|
+
.right-side-panel.right-side-panel-pinned
|
|
97
|
+
bottom calc(var(--shortcut-bar-h, 44px) + var(--shortcut-bar-kb-offset, 0px))
|
|
84
98
|
.qm-wrap-tooltip
|
|
85
99
|
bottom calc(var(--footer-stack-height, 36px) + var(--shortcut-bar-h, 44px) + var(--shortcut-bar-kb-offset, 0px))
|
|
86
100
|
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Global tooltip that detects Unix timestamps in terminal selections
|
|
3
3
|
* and displays their human-readable date/time near the cursor.
|
|
4
|
+
* Clicking the tooltip (or its copy icon) copies the formatted string.
|
|
4
5
|
* Registered via refsStatic as 'unix-timestamp-tooltip'.
|
|
5
6
|
*/
|
|
6
7
|
|
|
7
8
|
import { Component } from 'react'
|
|
9
|
+
import { CopyOutlined } from '@ant-design/icons'
|
|
8
10
|
import { refsStatic } from '../common/ref'
|
|
11
|
+
import { copy } from '../../common/clipboard'
|
|
9
12
|
|
|
10
13
|
export default class UnixTimestampTooltip extends Component {
|
|
11
14
|
state = {
|
|
@@ -56,6 +59,24 @@ export default class UnixTimestampTooltip extends Component {
|
|
|
56
59
|
}
|
|
57
60
|
}
|
|
58
61
|
|
|
62
|
+
handleHide = () => {
|
|
63
|
+
this.setState({ visible: false })
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Copy on mousedown instead of click: pressing the tooltip collapses the
|
|
68
|
+
* document selection, and the terminal drops its (DOM backed) selection on
|
|
69
|
+
* that change, which would unmount this tooltip before a click could land.
|
|
70
|
+
* preventDefault also keeps the terminal selection and focus untouched.
|
|
71
|
+
* The whole tooltip is the hit area, the icon is just the affordance.
|
|
72
|
+
*/
|
|
73
|
+
handleCopy = (e) => {
|
|
74
|
+
e.preventDefault()
|
|
75
|
+
e.stopPropagation()
|
|
76
|
+
copy(this.state.text)
|
|
77
|
+
this.handleHide()
|
|
78
|
+
}
|
|
79
|
+
|
|
59
80
|
render () {
|
|
60
81
|
const { visible, x, y, text } = this.state
|
|
61
82
|
if (!visible) {
|
|
@@ -63,6 +84,10 @@ export default class UnixTimestampTooltip extends Component {
|
|
|
63
84
|
}
|
|
64
85
|
return (
|
|
65
86
|
<div
|
|
87
|
+
className='unix-timestamp-tooltip pointer'
|
|
88
|
+
title={window.translate('copy')}
|
|
89
|
+
onMouseDown={this.handleCopy}
|
|
90
|
+
onMouseLeave={this.handleHide}
|
|
66
91
|
style={{
|
|
67
92
|
position: 'fixed',
|
|
68
93
|
left: x,
|
|
@@ -73,12 +98,13 @@ export default class UnixTimestampTooltip extends Component {
|
|
|
73
98
|
borderRadius: 4,
|
|
74
99
|
fontSize: 12,
|
|
75
100
|
whiteSpace: 'nowrap',
|
|
76
|
-
pointerEvents: 'none',
|
|
77
101
|
transform: 'translateX(-50%)',
|
|
102
|
+
userSelect: 'none',
|
|
78
103
|
zIndex: 9999
|
|
79
104
|
}}
|
|
80
105
|
>
|
|
81
106
|
{text}
|
|
107
|
+
<CopyOutlined style={{ marginLeft: 6 }} />
|
|
82
108
|
</div>
|
|
83
109
|
)
|
|
84
110
|
}
|
|
@@ -190,7 +190,7 @@ export default function ThemeAiEditor (props) {
|
|
|
190
190
|
const textAreaProps = {
|
|
191
191
|
value: description,
|
|
192
192
|
onChange: ev => setDescription(ev.target.value),
|
|
193
|
-
placeholder: e('
|
|
193
|
+
placeholder: e('description'),
|
|
194
194
|
autoSize: { minRows: 4, maxRows: 8 },
|
|
195
195
|
disabled: loading || disabled
|
|
196
196
|
}
|
|
@@ -223,7 +223,7 @@ export default function ThemeAiEditor (props) {
|
|
|
223
223
|
<div className='pd1t'>
|
|
224
224
|
<Space>
|
|
225
225
|
<Button {...generateBtnProps}>
|
|
226
|
-
{e('
|
|
226
|
+
{e('submit')}
|
|
227
227
|
</Button>
|
|
228
228
|
<HelpIcon link='https://github.com/electerm/electerm/wiki/Create-theme-by-AI' />
|
|
229
229
|
</Space>
|
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
validThemeProps,
|
|
15
15
|
requiredThemeProps
|
|
16
16
|
} from '../../common/terminal-theme'
|
|
17
|
-
import {
|
|
17
|
+
import { defaultThemeId, defaultThemeLightId } from '../../common/theme-defaults'
|
|
18
18
|
import generate from '../../common/uid'
|
|
19
19
|
import { isAIDisabled } from '../../common/ai-feature'
|
|
20
20
|
import Link from '../common/external-link'
|
|
@@ -272,7 +272,7 @@ export default function ThemeForm (props) {
|
|
|
272
272
|
themeName,
|
|
273
273
|
themeText: convertThemeToText(props.formData)
|
|
274
274
|
}
|
|
275
|
-
const isDefaultTheme = id ===
|
|
275
|
+
const isDefaultTheme = id === defaultThemeId || id === defaultThemeLightId
|
|
276
276
|
const disabled = readonly || isDefaultTheme
|
|
277
277
|
const pickerProps = {
|
|
278
278
|
onChange: onPickerChange,
|
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
* theme list render
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import { useState } from 'react'
|
|
5
|
+
import { useEffect, useState } from 'react'
|
|
6
6
|
import {
|
|
7
7
|
CheckCircleOutlined,
|
|
8
|
+
CloseOutlined,
|
|
8
9
|
PlusOutlined,
|
|
9
10
|
SunOutlined,
|
|
10
|
-
MoonOutlined
|
|
11
|
-
EyeOutlined
|
|
11
|
+
MoonOutlined
|
|
12
12
|
} from '@ant-design/icons'
|
|
13
13
|
import { Tag, Tooltip, Button, Space } from 'antd'
|
|
14
14
|
import classnames from 'classnames'
|
|
@@ -30,46 +30,72 @@ export default function ThemeListItem (props) {
|
|
|
30
30
|
const [tooltipVisible, setTooltipVisible] = useState(false)
|
|
31
31
|
const [isPreviewing, setIsPreviewing] = useState(false)
|
|
32
32
|
|
|
33
|
-
function
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
33
|
+
function startPreview () {
|
|
34
|
+
// keep the very first original, hopping between themes must never
|
|
35
|
+
// overwrite it with a theme that is itself only a preview
|
|
36
|
+
if (!window.originalTheme) {
|
|
37
|
+
window.originalTheme = store.config.theme
|
|
38
|
+
}
|
|
37
39
|
store.setTheme(item.id)
|
|
40
|
+
setIsPreviewing(true)
|
|
38
41
|
}
|
|
39
42
|
|
|
40
|
-
function
|
|
41
|
-
if (
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
window.originalTheme = currentTheme
|
|
45
|
-
// Apply the preview theme
|
|
46
|
-
store.setTheme(item.id)
|
|
47
|
-
setIsPreviewing(true)
|
|
43
|
+
function cancelPreview () {
|
|
44
|
+
if (window.originalTheme) {
|
|
45
|
+
store.setTheme(window.originalTheme)
|
|
46
|
+
delete window.originalTheme
|
|
48
47
|
}
|
|
48
|
+
setIsPreviewing(false)
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
//
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
return () => {
|
|
53
|
+
// leaving the list mid preview (switching settings tab, filtering the
|
|
54
|
+
// list, ...) must not leave the user stuck on a theme never applied
|
|
55
55
|
if (window.originalTheme) {
|
|
56
56
|
store.setTheme(window.originalTheme)
|
|
57
57
|
delete window.originalTheme
|
|
58
58
|
}
|
|
59
|
-
|
|
59
|
+
}
|
|
60
|
+
}, [])
|
|
61
|
+
|
|
62
|
+
function handleClickApply () {
|
|
63
|
+
delete window.originalTheme
|
|
64
|
+
setIsPreviewing(false)
|
|
65
|
+
setTooltipVisible(false)
|
|
66
|
+
store.setTheme(item.id)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function handleClickCancel () {
|
|
70
|
+
cancelPreview()
|
|
71
|
+
setTooltipVisible(false)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function handleTooltipVisibleChange (visible) {
|
|
75
|
+
setTooltipVisible(visible)
|
|
76
|
+
// opening the popup is the preview itself; every other way of closing it
|
|
77
|
+
// (click elsewhere, esc, clicking the icon again) rolls the preview back
|
|
78
|
+
if (visible) {
|
|
79
|
+
startPreview()
|
|
80
|
+
} else {
|
|
81
|
+
cancelPreview()
|
|
60
82
|
}
|
|
61
83
|
}
|
|
62
84
|
|
|
85
|
+
function handleClickApplyIcon (e) {
|
|
86
|
+
// previewing must not also load the theme into the edit form
|
|
87
|
+
e.stopPropagation()
|
|
88
|
+
}
|
|
89
|
+
|
|
63
90
|
function renderTooltipContent () {
|
|
64
91
|
return (
|
|
65
92
|
<Space.Compact>
|
|
66
93
|
<Button
|
|
67
94
|
size='small'
|
|
68
|
-
icon={<
|
|
69
|
-
onClick={
|
|
70
|
-
type={isPreviewing ? 'primary' : 'default'}
|
|
95
|
+
icon={<CloseOutlined />}
|
|
96
|
+
onClick={handleClickCancel}
|
|
71
97
|
>
|
|
72
|
-
{e('
|
|
98
|
+
{e('cancel')}
|
|
73
99
|
</Button>
|
|
74
100
|
<Button
|
|
75
101
|
size='small'
|
|
@@ -96,7 +122,15 @@ export default function ThemeListItem (props) {
|
|
|
96
122
|
placement='top'
|
|
97
123
|
>
|
|
98
124
|
<CheckCircleOutlined
|
|
99
|
-
className=
|
|
125
|
+
className={
|
|
126
|
+
classnames(
|
|
127
|
+
'pointer list-item-apply',
|
|
128
|
+
{
|
|
129
|
+
'list-item-apply-previewing': isPreviewing
|
|
130
|
+
}
|
|
131
|
+
)
|
|
132
|
+
}
|
|
133
|
+
onClick={handleClickApplyIcon}
|
|
100
134
|
/>
|
|
101
135
|
</Tooltip>
|
|
102
136
|
)
|