@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.
Files changed (40) hide show
  1. package/README.md +12 -2
  2. package/client/common/constants.js +14 -0
  3. package/client/common/default-setting.js +2 -2
  4. package/client/common/parse-vv.js +313 -0
  5. package/client/common/theme-defaults.js +5 -2
  6. package/client/common/update-check.js +5 -4
  7. package/client/components/ai/ai-presets.js +13 -2
  8. package/client/components/bg/css-overwrite.jsx +16 -1
  9. package/client/components/bookmark-form/common/fields.jsx +3 -0
  10. package/client/components/bookmark-form/common/vv-file-field.jsx +137 -0
  11. package/client/components/bookmark-form/config/spice.js +1 -0
  12. package/client/components/footer/footer-entry.jsx +39 -14
  13. package/client/components/icons/ai-icon.styl +4 -1
  14. package/client/components/layout/layout.jsx +7 -1
  15. package/client/components/main/main.jsx +9 -2
  16. package/client/components/main/term-fullscreen.styl +7 -0
  17. package/client/components/quick-commands/quick-command-ai-editor.jsx +255 -0
  18. package/client/components/quick-commands/quick-command-ai-editor.styl +13 -0
  19. package/client/components/quick-commands/quick-commands-box.jsx +23 -5
  20. package/client/components/quick-commands/quick-commands-form-elem.jsx +129 -65
  21. package/client/components/setting-panel/list.jsx +2 -2
  22. package/client/components/side-panel-r/right-side-panel.styl +11 -1
  23. package/client/components/side-panel-r/side-panel-r.jsx +45 -9
  24. package/client/components/sidebar/bookmark-select.jsx +3 -1
  25. package/client/components/sidebar/index.jsx +13 -4
  26. package/client/components/sidebar/side-panel.jsx +8 -3
  27. package/client/components/sidebar/sidebar-panel.jsx +20 -7
  28. package/client/components/terminal/shortcut-bar.styl +14 -0
  29. package/client/components/terminal/unix-timestamp-tooltip.jsx +27 -1
  30. package/client/components/terminal-info/terminal-info.styl +3 -0
  31. package/client/components/theme/terminal-theme-list.styl +3 -0
  32. package/client/components/theme/theme-ai-editor.jsx +2 -2
  33. package/client/components/theme/theme-form.jsx +2 -2
  34. package/client/components/theme/theme-list-item.jsx +59 -25
  35. package/client/css/mobile.styl +28 -0
  36. package/client/store/common.js +44 -0
  37. package/client/store/init-state.js +2 -1
  38. package/client/store/sidebar.js +5 -1
  39. package/client/store/store.js +9 -2
  40. 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
- <FormItem
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 { settingSyncId, settingCommonId, staticNewItemTabs } from '../../common/constants'
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 || [settingSyncId, settingCommonId].includes(item.id) || item.id.startsWith('default')) {
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
- bottom 0
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.rightPanelPinned = !window.store.rightPanelPinned
79
+ window.store.setRightPanelPinned(!window.store.rightPanelPinned)
50
80
  }
51
81
 
52
82
  const panelProps = {
53
- className: 'right-side-panel animate-fast' + (rightPanelPinned ? ' right-side-panel-pinned' : ''),
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' + (rightPanelPinned ? ' pinned' : ''),
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
- <DragHandle {...dragProps} />
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
- <PushpinOutlined
87
- {...pinProps}
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
- if (!store.pinned) {
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
- if (!store.openedSideBar || store.pinned || hasActiveInput()) {
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={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
- <DragHandle
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
- <Tooltip title={e('pin')}>
104
- <PushpinOutlined
105
- {...prps1}
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
  }
@@ -2,6 +2,9 @@
2
2
  color var(--text-dark)
3
3
  &:hover
4
4
  color var(--text)
5
+ // panel open: stay lit (after :hover so the "on" state survives hovering)
6
+ &.active
7
+ color var(--success)
5
8
  .terminal-info-section,
6
9
  .right-side-panel-content > .remote-monitor-details
7
10
  padding 10px 0
@@ -1,3 +1,6 @@
1
1
 
2
2
  .theme-item.current
3
3
  border-left 3px solid var(--success)
4
+
5
+ .list-item-apply-previewing
6
+ color var(--success)
@@ -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('createThemeByAI'),
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('generate')}
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 { defaultTheme, defaultThemeLight } from '../../common/theme-defaults'
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 === defaultTheme().id || id === defaultThemeLight().id
275
+ const isDefaultTheme = id === defaultThemeId || id === defaultThemeLightId
276
276
  const disabled = readonly || isDefaultTheme
277
277
  const pickerProps = {
278
278
  onChange: onPickerChange,