@electerm/electerm-react 5.5.15 → 5.5.25
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 +12 -2
- package/client/common/constants.js +14 -0
- package/client/common/default-setting.js +2 -2
- package/client/common/parse-vv.js +313 -0
- package/client/common/theme-defaults.js +5 -2
- package/client/common/update-check.js +5 -4
- package/client/components/ai/ai-presets.js +13 -2
- 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 +137 -0
- package/client/components/bookmark-form/config/spice.js +1 -0
- package/client/components/footer/footer-entry.jsx +39 -14
- package/client/components/icons/ai-icon.styl +4 -1
- package/client/components/layout/layout.jsx +7 -1
- package/client/components/main/main.jsx +9 -2
- package/client/components/main/term-fullscreen.styl +7 -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 +23 -5
- package/client/components/quick-commands/quick-commands-form-elem.jsx +129 -65
- package/client/components/setting-panel/list.jsx +2 -2
- package/client/components/side-panel-r/right-side-panel.styl +11 -1
- package/client/components/side-panel-r/side-panel-r.jsx +45 -9
- package/client/components/sidebar/bookmark-select.jsx +3 -1
- 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/css/mobile.styl +28 -0
- package/client/store/common.js +44 -0
- package/client/store/init-state.js +2 -1
- package/client/store/sidebar.js +5 -1
- package/client/store/store.js +9 -2
- package/package.json +1 -1
|
@@ -4,12 +4,16 @@ import {
|
|
|
4
4
|
Select,
|
|
5
5
|
Input
|
|
6
6
|
} from 'antd'
|
|
7
|
+
import { RobotOutlined } from '@ant-design/icons'
|
|
7
8
|
import message from '../common/message'
|
|
8
9
|
import SwitchLabel from '../common/switch'
|
|
9
10
|
import { useState } from 'react'
|
|
10
11
|
import generate from '../../common/uid'
|
|
11
12
|
import InputAutoFocus from '../common/input-auto-focus'
|
|
12
13
|
import renderQm from './quick-commands-list-form'
|
|
14
|
+
import QuickCommandAiEditor from './quick-command-ai-editor'
|
|
15
|
+
import ResponsiveTabs from '../common/responsive-tabs'
|
|
16
|
+
import { isAIDisabled } from '../../common/ai-feature'
|
|
13
17
|
import ShortcutEdit from '../shortcuts/shortcut-editor'
|
|
14
18
|
import { getKeysTakenData } from '../shortcuts/shortcut-utils'
|
|
15
19
|
import deepCopy from 'json-deep-copy'
|
|
@@ -18,6 +22,8 @@ import HelpIcon from '../common/help-icon'
|
|
|
18
22
|
|
|
19
23
|
const FormItem = Form.Item
|
|
20
24
|
const { Option } = Select
|
|
25
|
+
const manualTab = 'qm-form-manual'
|
|
26
|
+
const aiTab = 'qm-form-ai'
|
|
21
27
|
const e = window.translate
|
|
22
28
|
|
|
23
29
|
export default function QuickCommandForm (props) {
|
|
@@ -25,6 +31,7 @@ export default function QuickCommandForm (props) {
|
|
|
25
31
|
const { store, formData } = props
|
|
26
32
|
const { quickCommandTags = [] } = store
|
|
27
33
|
const [shortcut, setShortcut] = useState(formData.shortcut || '')
|
|
34
|
+
const [tab, setTab] = useState(manualTab)
|
|
28
35
|
const uid = formData.id || generate()
|
|
29
36
|
const updateConfig = (name, value) => {
|
|
30
37
|
form.setFieldsValue({
|
|
@@ -86,6 +93,22 @@ export default function QuickCommandForm (props) {
|
|
|
86
93
|
message.success(e('saved'))
|
|
87
94
|
props.onSaved?.()
|
|
88
95
|
}
|
|
96
|
+
|
|
97
|
+
// AI generated quick command: write it into the same form (so the user can
|
|
98
|
+
// review/edit it) and jump back to the manual tab, where the save button is
|
|
99
|
+
function handleAiGenerated (data) {
|
|
100
|
+
const update = {
|
|
101
|
+
name: data.name,
|
|
102
|
+
commands: data.commands,
|
|
103
|
+
inputOnly: data.inputOnly
|
|
104
|
+
}
|
|
105
|
+
if (data.labels.length) {
|
|
106
|
+
update.labels = data.labels
|
|
107
|
+
}
|
|
108
|
+
form.setFieldsValue(update)
|
|
109
|
+
setTab(manualTab)
|
|
110
|
+
}
|
|
111
|
+
|
|
89
112
|
const initialValues = formData
|
|
90
113
|
if (!initialValues.labels) {
|
|
91
114
|
initialValues.labels = []
|
|
@@ -112,6 +135,111 @@ export default function QuickCommandForm (props) {
|
|
|
112
135
|
return `{{${t}}}`
|
|
113
136
|
}).join(', ')
|
|
114
137
|
const wiki = 'https://github.com/electerm/electerm/wiki/quick-command-templates'
|
|
138
|
+
const aiDisabled = isAIDisabled()
|
|
139
|
+
// renderQm() uses hooks internally, so it must run on every render
|
|
140
|
+
const qmItem = renderQm(form)
|
|
141
|
+
const manualPane = (
|
|
142
|
+
<>
|
|
143
|
+
<FormItem
|
|
144
|
+
label={e('quickCommandName')}
|
|
145
|
+
rules={[{
|
|
146
|
+
max: 60, message: '60 chars max'
|
|
147
|
+
}, {
|
|
148
|
+
required: true, message: 'Name required'
|
|
149
|
+
}]}
|
|
150
|
+
hasFeedback
|
|
151
|
+
name='name'
|
|
152
|
+
>
|
|
153
|
+
<InputAutoFocus />
|
|
154
|
+
</FormItem>
|
|
155
|
+
{qmItem}
|
|
156
|
+
<FormItem
|
|
157
|
+
name='labels'
|
|
158
|
+
label={e('label')}
|
|
159
|
+
>
|
|
160
|
+
<Select
|
|
161
|
+
mode='tags'
|
|
162
|
+
>
|
|
163
|
+
{
|
|
164
|
+
quickCommandTags.map(q => {
|
|
165
|
+
return (
|
|
166
|
+
<Option value={q} key={'qmt-' + q}>
|
|
167
|
+
{q}
|
|
168
|
+
</Option>
|
|
169
|
+
)
|
|
170
|
+
})
|
|
171
|
+
}
|
|
172
|
+
</Select>
|
|
173
|
+
</FormItem>
|
|
174
|
+
<FormItem
|
|
175
|
+
label={e('settingShortcuts')}
|
|
176
|
+
name='shortcut'
|
|
177
|
+
>
|
|
178
|
+
<div>
|
|
179
|
+
<Input className='hide' />
|
|
180
|
+
<ShortcutEdit
|
|
181
|
+
{...editorProps}
|
|
182
|
+
/>
|
|
183
|
+
</div>
|
|
184
|
+
</FormItem>
|
|
185
|
+
<FormItem
|
|
186
|
+
label={e('inputOnly')}
|
|
187
|
+
name='inputOnly'
|
|
188
|
+
valuePropName='checked'
|
|
189
|
+
>
|
|
190
|
+
<SwitchLabel />
|
|
191
|
+
</FormItem>
|
|
192
|
+
<FormItem>
|
|
193
|
+
<p>
|
|
194
|
+
<Button
|
|
195
|
+
type='primary'
|
|
196
|
+
htmlType='submit'
|
|
197
|
+
>{e('save')}
|
|
198
|
+
</Button>
|
|
199
|
+
</p>
|
|
200
|
+
</FormItem>
|
|
201
|
+
<p>
|
|
202
|
+
<b className='mg1r'>{e('templates')}:</b>
|
|
203
|
+
<span className='mg1r'>{templatesStr}</span>
|
|
204
|
+
<HelpIcon
|
|
205
|
+
link={wiki}
|
|
206
|
+
/>
|
|
207
|
+
</p>
|
|
208
|
+
</>
|
|
209
|
+
)
|
|
210
|
+
const tabsProps = {
|
|
211
|
+
activeKey: tab,
|
|
212
|
+
onChange: setTab,
|
|
213
|
+
className: 'qm-form-tabs',
|
|
214
|
+
size: 'small',
|
|
215
|
+
items: [
|
|
216
|
+
{
|
|
217
|
+
key: manualTab,
|
|
218
|
+
label: e('quickCommand')
|
|
219
|
+
},
|
|
220
|
+
...(!aiDisabled
|
|
221
|
+
? [{
|
|
222
|
+
key: aiTab,
|
|
223
|
+
label: (
|
|
224
|
+
<span className='qm-form-tab-ai'>
|
|
225
|
+
<RobotOutlined className='mg1r' />
|
|
226
|
+
AI
|
|
227
|
+
</span>
|
|
228
|
+
)
|
|
229
|
+
}]
|
|
230
|
+
: [])
|
|
231
|
+
]
|
|
232
|
+
}
|
|
233
|
+
const aiPane = (
|
|
234
|
+
<>
|
|
235
|
+
<ResponsiveTabs {...tabsProps} />
|
|
236
|
+
{
|
|
237
|
+
tab === aiTab
|
|
238
|
+
? <QuickCommandAiEditor onChange={handleAiGenerated} />
|
|
239
|
+
: manualPane
|
|
240
|
+
}
|
|
241
|
+
</>
|
|
242
|
+
)
|
|
115
243
|
return (
|
|
116
244
|
<>
|
|
117
245
|
<Form
|
|
@@ -121,71 +249,7 @@ export default function QuickCommandForm (props) {
|
|
|
121
249
|
layout='vertical'
|
|
122
250
|
initialValues={initialValues}
|
|
123
251
|
>
|
|
124
|
-
|
|
125
|
-
label={e('quickCommandName')}
|
|
126
|
-
rules={[{
|
|
127
|
-
max: 60, message: '60 chars max'
|
|
128
|
-
}, {
|
|
129
|
-
required: true, message: 'Name required'
|
|
130
|
-
}]}
|
|
131
|
-
hasFeedback
|
|
132
|
-
name='name'
|
|
133
|
-
>
|
|
134
|
-
<InputAutoFocus />
|
|
135
|
-
</FormItem>
|
|
136
|
-
{renderQm(form)}
|
|
137
|
-
<FormItem
|
|
138
|
-
name='labels'
|
|
139
|
-
label={e('label')}
|
|
140
|
-
>
|
|
141
|
-
<Select
|
|
142
|
-
mode='tags'
|
|
143
|
-
>
|
|
144
|
-
{
|
|
145
|
-
quickCommandTags.map(q => {
|
|
146
|
-
return (
|
|
147
|
-
<Option value={q} key={'qmt-' + q}>
|
|
148
|
-
{q}
|
|
149
|
-
</Option>
|
|
150
|
-
)
|
|
151
|
-
})
|
|
152
|
-
}
|
|
153
|
-
</Select>
|
|
154
|
-
</FormItem>
|
|
155
|
-
<FormItem
|
|
156
|
-
label={e('settingShortcuts')}
|
|
157
|
-
name='shortcut'
|
|
158
|
-
>
|
|
159
|
-
<div>
|
|
160
|
-
<Input className='hide' />
|
|
161
|
-
<ShortcutEdit
|
|
162
|
-
{...editorProps}
|
|
163
|
-
/>
|
|
164
|
-
</div>
|
|
165
|
-
</FormItem>
|
|
166
|
-
<FormItem
|
|
167
|
-
label={e('inputOnly')}
|
|
168
|
-
name='inputOnly'
|
|
169
|
-
valuePropName='checked'
|
|
170
|
-
>
|
|
171
|
-
<SwitchLabel />
|
|
172
|
-
</FormItem>
|
|
173
|
-
<FormItem>
|
|
174
|
-
<p>
|
|
175
|
-
<Button
|
|
176
|
-
type='primary'
|
|
177
|
-
htmlType='submit'
|
|
178
|
-
>{e('save')}
|
|
179
|
-
</Button>
|
|
180
|
-
</p>
|
|
181
|
-
</FormItem>
|
|
182
|
-
<p>
|
|
183
|
-
<b className='mg1r'>{e('templates')}:</b>
|
|
184
|
-
<span className='mg1r'>{templatesStr}</span>
|
|
185
|
-
<HelpIcon
|
|
186
|
-
link={wiki}
|
|
187
|
-
/>
|
|
188
|
-
</p>
|
|
252
|
+
{aiDisabled ? manualPane : aiPane}
|
|
189
253
|
</Form>
|
|
190
254
|
</>
|
|
191
255
|
)
|
|
@@ -9,7 +9,7 @@ import createName, { createTitleTag } from '../../common/create-title'
|
|
|
9
9
|
import classnames from 'classnames'
|
|
10
10
|
import { noop } from 'lodash-es'
|
|
11
11
|
import highlight from '../common/highlight'
|
|
12
|
-
import {
|
|
12
|
+
import { staticNewItemTabs, undeletableIds } from '../../common/constants'
|
|
13
13
|
import getInitItem from '../../common/init-setting-item'
|
|
14
14
|
|
|
15
15
|
const e = window.translate
|
|
@@ -64,7 +64,7 @@ export default class ItemList extends React.PureComponent {
|
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
renderDelBtn = item => {
|
|
67
|
-
if (!item.id ||
|
|
67
|
+
if (!item.id || undeletableIds.has(item.id)) {
|
|
68
68
|
return null
|
|
69
69
|
}
|
|
70
70
|
const { shouldConfirmDel } = this.props
|
|
@@ -2,13 +2,23 @@
|
|
|
2
2
|
position absolute
|
|
3
3
|
right 0
|
|
4
4
|
top 36px
|
|
5
|
-
|
|
5
|
+
// Unpinned is an overlay, so stop above the footer: the footer is app chrome
|
|
6
|
+
// and holds the very icon that toggles this panel, so a full-height overlay
|
|
7
|
+
// would bury its own trigger. 36px is footerHeight (common/constants.js).
|
|
8
|
+
// Two cases move or remove the footer, and are handled where their sibling
|
|
9
|
+
// rules already live — see shortcut-bar.styl (.shortcut-bar-on lifts the
|
|
10
|
+
// footer) and term-fullscreen.styl (rdp/vnc/spice fullscreen hides it).
|
|
11
|
+
bottom 36px
|
|
6
12
|
z-index 200
|
|
7
13
|
background var(--main)
|
|
8
14
|
color var(--text)
|
|
9
15
|
border-left 1px solid var(--main-darker)
|
|
16
|
+
// Pinned is a dock, not an overlay: it owns the whole right column, top to
|
|
17
|
+
// bottom. The layout squeezes the terminal out of the way instead
|
|
18
|
+
// (layout.jsx reserves rightPanelWidth), so covering the footer is intended.
|
|
10
19
|
&.right-side-panel-pinned
|
|
11
20
|
top 0
|
|
21
|
+
bottom 0
|
|
12
22
|
|
|
13
23
|
.drag-handle
|
|
14
24
|
left 0
|
|
@@ -1,4 +1,4 @@
|
|
|
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 {
|
|
@@ -19,14 +19,44 @@ export default memo(function RightSidePanel (
|
|
|
19
19
|
rightPanelWidth,
|
|
20
20
|
children,
|
|
21
21
|
title,
|
|
22
|
-
rightPanelTab
|
|
22
|
+
rightPanelTab,
|
|
23
|
+
isMobile
|
|
23
24
|
}
|
|
24
25
|
) {
|
|
25
26
|
const panelRef = useRef(null)
|
|
26
27
|
|
|
28
|
+
// Esc closes the panel, but never at the terminal's expense: xterm forwards
|
|
29
|
+
// Esc to the remote shell, so stealing it there would break vim, less, etc.
|
|
30
|
+
// The guard checks the event target rather than focus state, because the
|
|
31
|
+
// keydown bubbles up from xterm's textarea inside .term-wrap — the same
|
|
32
|
+
// selector shortcut-bar-entry.jsx uses to detect terminal focus. A modal owns
|
|
33
|
+
// Esc while one is open. This hook has to stay above the early return below
|
|
34
|
+
// so the hook order never changes.
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
if (!rightPanelVisible) {
|
|
37
|
+
return
|
|
38
|
+
}
|
|
39
|
+
function onKeyDown (event) {
|
|
40
|
+
if (event.key !== 'Escape' || window.store.showModal) {
|
|
41
|
+
return
|
|
42
|
+
}
|
|
43
|
+
const { target } = event
|
|
44
|
+
if (target && typeof target.closest === 'function' && target.closest('.term-wrap')) {
|
|
45
|
+
return
|
|
46
|
+
}
|
|
47
|
+
window.store.rightPanelVisible = false
|
|
48
|
+
}
|
|
49
|
+
document.addEventListener('keydown', onKeyDown)
|
|
50
|
+
return () => document.removeEventListener('keydown', onKeyDown)
|
|
51
|
+
}, [rightPanelVisible])
|
|
52
|
+
|
|
27
53
|
if (!rightPanelVisible) {
|
|
28
54
|
return null
|
|
29
55
|
}
|
|
56
|
+
// Mobile has no pin control (it is not rendered below), so a pin persisted
|
|
57
|
+
// from a desktop session must not turn the panel into a dock here — that
|
|
58
|
+
// would make it full height over the footer for no reachable reason.
|
|
59
|
+
const pinned = rightPanelPinned && !isMobile
|
|
30
60
|
const tag = rightPanelTab === 'ai'
|
|
31
61
|
? <Tag className='mg1r'>AI</Tag>
|
|
32
62
|
: <BarChartOutlined className='mg1r' />
|
|
@@ -46,11 +76,11 @@ export default memo(function RightSidePanel (
|
|
|
46
76
|
}
|
|
47
77
|
|
|
48
78
|
function togglePin () {
|
|
49
|
-
window.store.
|
|
79
|
+
window.store.setRightPanelPinned(!window.store.rightPanelPinned)
|
|
50
80
|
}
|
|
51
81
|
|
|
52
82
|
const panelProps = {
|
|
53
|
-
className: 'right-side-panel animate-fast' + (
|
|
83
|
+
className: 'right-side-panel animate-fast' + (pinned ? ' right-side-panel-pinned' : ''),
|
|
54
84
|
ref: panelRef,
|
|
55
85
|
style: {
|
|
56
86
|
width: `${rightPanelWidth}px`
|
|
@@ -58,7 +88,7 @@ export default memo(function RightSidePanel (
|
|
|
58
88
|
}
|
|
59
89
|
|
|
60
90
|
const pinProps = {
|
|
61
|
-
className: 'right-side-panel-pin right-side-panel-controls' + (
|
|
91
|
+
className: 'right-side-panel-pin right-side-panel-controls' + (pinned ? ' pinned' : ''),
|
|
62
92
|
onClick: togglePin
|
|
63
93
|
}
|
|
64
94
|
const dragProps = {
|
|
@@ -73,7 +103,11 @@ export default memo(function RightSidePanel (
|
|
|
73
103
|
<div
|
|
74
104
|
{...panelProps}
|
|
75
105
|
>
|
|
76
|
-
|
|
106
|
+
{/* Mobile panels are full-screen drawers (see store.rightPanelWidth):
|
|
107
|
+
their width is the viewport and is not adjustable, so the resize
|
|
108
|
+
handle is not rendered — it would otherwise sit on the screen's left
|
|
109
|
+
edge and drag a size the panel never uses. */}
|
|
110
|
+
{!isMobile && <DragHandle {...dragProps} />}
|
|
77
111
|
<Flex
|
|
78
112
|
className='right-panel-title pd2'
|
|
79
113
|
justify='space-between'
|
|
@@ -83,9 +117,11 @@ export default memo(function RightSidePanel (
|
|
|
83
117
|
{tag} {title}
|
|
84
118
|
</Typography.Text>
|
|
85
119
|
<Flex>
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
120
|
+
{!isMobile && (
|
|
121
|
+
<PushpinOutlined
|
|
122
|
+
{...pinProps}
|
|
123
|
+
/>
|
|
124
|
+
)}
|
|
89
125
|
<CloseCircleOutlined
|
|
90
126
|
className='right-side-panel-close right-side-panel-controls mg1l'
|
|
91
127
|
onClick={onClose}
|
|
@@ -20,7 +20,9 @@ export default auto(function BookmarkSelect (props) {
|
|
|
20
20
|
return null
|
|
21
21
|
}
|
|
22
22
|
const onClickItem = (item) => {
|
|
23
|
-
|
|
23
|
+
// A pinned (docked) panel stays open; anything else — including mobile,
|
|
24
|
+
// where pinning is not a reachable mode — dismisses on selection.
|
|
25
|
+
if (!(store.pinned && !store.isMobile)) {
|
|
24
26
|
store.setOpenedSideBar('')
|
|
25
27
|
}
|
|
26
28
|
store.onSelectBookmark(item.id)
|
|
@@ -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,
|