@electerm/electerm-react 3.15.152 → 3.15.155

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.
@@ -197,6 +197,7 @@ export const commonLineEndings = commonTxLineEndings
197
197
 
198
198
  export const maxBatchInput = 30
199
199
  export const windowControlWidth = 94
200
+ export const mobileBreakpoint = 800
200
201
  export const baseUpdateCheckUrls = [
201
202
  packInfo.homepage,
202
203
  'https://gitee.com/github-zxdong262/electerm/raw/gh-pages'
@@ -16,6 +16,15 @@
16
16
  .number-input
17
17
  min-width 210px
18
18
 
19
+ // On mobile, stack loginScriptDelay and loginScript vertically instead of
20
+ // horizontally (the antd Space renders them side by side by default).
21
+ .is-mobile
22
+ .run-scripts-space.ant-space
23
+ flex-direction column
24
+ align-items stretch
25
+ .ant-space-item
26
+ width 100%
27
+
19
28
  .tree-select-wrapper
20
29
  display flex
21
30
  flex-direction column
@@ -18,7 +18,7 @@ export default function renderRunScripts () {
18
18
  <>
19
19
  <Space
20
20
  align='baseline'
21
- className='width-100'
21
+ className='width-100 run-scripts-space'
22
22
  key={field.key}
23
23
  >
24
24
  <FormItem
@@ -1,14 +1,23 @@
1
1
  /**
2
2
  * batch input module
3
+ *
4
+ * Unified click-to-open behavior for mobile and desktop:
5
+ * - click the compact trigger -> expand the textarea + tab-select panel
6
+ * - click outside the panel -> collapse it
7
+ * - press Enter in the textarea -> send `cmd` to all selected tabs
8
+ *
9
+ * On desktop the compact trigger is rendered as a read-only Input that only
10
+ * serves as a clickable affordance — it is never a real input. On mobile it
11
+ * is a small "B" button to save horizontal space. The open/close logic is
12
+ * identical for both.
3
13
  */
4
14
 
5
- import { Component } from 'react'
15
+ import { Component, createRef } from 'react'
6
16
  import {
7
- AutoComplete,
17
+ Button,
8
18
  Input
9
19
  } from 'antd'
10
20
  import {
11
- batchInputLsKey,
12
21
  terminalWebType,
13
22
  terminalRdpType,
14
23
  terminalVncType
@@ -24,97 +33,71 @@ export default class BatchInput extends Component {
24
33
  super(props)
25
34
  this.state = {
26
35
  cmd: '',
27
- open: false,
28
- enter: false
36
+ open: false
29
37
  }
38
+ this.outerRef = createRef()
39
+ this.textAreaRef = createRef()
40
+ }
41
+
42
+ componentDidMount () {
43
+ document.addEventListener('mousedown', this.handleDocClick)
30
44
  }
31
45
 
32
46
  componentWillUnmount () {
33
- clearTimeout(this.timer)
47
+ document.removeEventListener('mousedown', this.handleDocClick)
34
48
  }
35
49
 
36
- handleEnter = (e) => {
37
- const { batchInputSelectedTabIds } = window.store
38
- const { cmd } = this.state
39
- if (!cmd.trim()) {
50
+ // Collapse the panel when the user clicks anywhere outside of it.
51
+ // Clicks inside an antd Popover (e.g. the tab-select popover, which renders
52
+ // in a portal outside this node) are treated as "inside" so interacting with
53
+ // tab selection does not collapse the panel.
54
+ handleDocClick = (ev) => {
55
+ if (!this.state.open) {
40
56
  return
41
57
  }
42
- window.store.addBatchInput(cmd)
43
- this.props.input(cmd, Array.from(batchInputSelectedTabIds))
44
- this.setState({
45
- cmd: '',
46
- open: false
47
- })
48
- e.stopPropagation()
49
- }
50
-
51
- handleChange = (v = '') => {
52
- let vv = v.replace(/^\d+:/, '').replace(/\n$/, '')
53
- if (vv === batchInputLsKey) {
54
- window.store.clearBatchInput()
55
- vv = ''
58
+ const node = this.outerRef.current
59
+ if (node && node.contains(ev.target)) {
60
+ return
56
61
  }
57
- this.setState({
58
- cmd: vv,
59
- open: false
60
- })
61
- }
62
-
63
- handleClick = () => {
64
- this.setState({
65
- open: true
66
- })
67
- window.store.filterBatchInputSelectedTabIds()
68
- }
69
-
70
- handleBlur = () => {
71
- this.setState({
72
- open: false
73
- })
74
- }
75
-
76
- mapper = (v, i) => {
77
- return {
78
- value: `${i}:${v}`,
79
- label: v
62
+ if (ev.target && ev.target.closest && ev.target.closest('.ant-popover')) {
63
+ return
80
64
  }
65
+ this.setState({ open: false })
81
66
  }
82
67
 
83
- renderClear = () => {
84
- return {
85
- value: batchInputLsKey,
86
- label: e('clear')
68
+ handleToggle = () => {
69
+ const willOpen = !this.state.open
70
+ if (willOpen) {
71
+ window.store.filterBatchInputSelectedTabIds()
87
72
  }
73
+ this.setState({ open: willOpen }, () => {
74
+ if (willOpen && this.textAreaRef.current) {
75
+ this.textAreaRef.current.focus()
76
+ }
77
+ })
88
78
  }
89
79
 
90
- buildOptions = () => {
91
- const arr = this.props.batchInputs.map(this.mapper)
92
- if (arr.length) {
93
- return [
94
- ...arr,
95
- this.renderClear()
96
- ]
80
+ handleSubmit = (ev) => {
81
+ const { batchInputSelectedTabIds } = this.props
82
+ const { cmd } = this.state
83
+ if (!cmd.trim()) {
84
+ return
97
85
  }
98
- return []
99
- }
100
-
101
- handleMouseEnter = () => {
102
- clearTimeout(this.timer)
86
+ window.store.addBatchInput(cmd)
87
+ this.props.input(cmd, Array.from(batchInputSelectedTabIds))
103
88
  this.setState({
104
- enter: true
89
+ cmd: '',
90
+ open: false
105
91
  })
92
+ ev?.stopPropagation()
106
93
  }
107
94
 
108
- leave = () => {
95
+ handleChange = (ev) => {
109
96
  this.setState({
110
- enter: false
97
+ cmd: ev.target.value
111
98
  })
112
99
  }
113
100
 
114
- handleMouseLeave = () => {
115
- this.timer = setTimeout(this.leave, 5000)
116
- }
117
-
118
101
  getTabs = () => {
119
102
  const { activeTabId } = this.props
120
103
  return deepCopy(this.props.tabs.filter(tab => {
@@ -122,43 +105,54 @@ export default class BatchInput extends Component {
122
105
  tab.type !== terminalRdpType &&
123
106
  tab.type !== terminalVncType
124
107
  })).sort((a, b) => {
125
- // Current tab goes first
108
+ // current tab goes first
126
109
  if (a.id === activeTabId) return -1
127
110
  if (b.id === activeTabId) return 1
128
111
  return 0
129
112
  })
130
113
  }
131
114
 
132
- render () {
133
- const { cmd, open, enter } = this.state
134
- const {
135
- batchInputSelectedTabIds
136
- } = this.props
137
- const opts = {
138
- options: this.buildOptions(),
139
- value: cmd,
140
- onChange: this.handleChange,
141
- defaultOpen: false,
142
- open,
143
- className: 'batch-input-wrap'
115
+ renderTrigger () {
116
+ const { isMobile } = this.props
117
+ if (isMobile) {
118
+ return (
119
+ <Button
120
+ size='small'
121
+ type='text'
122
+ onClick={this.handleToggle}
123
+ >
124
+ B
125
+ </Button>
126
+ )
144
127
  }
128
+ // Desktop: an input-shaped, read-only affordance. It is NOT a real input —
129
+ // clicking it expands the panel where the user actually types.
130
+ return (
131
+ <Input
132
+ size='small'
133
+ readOnly
134
+ placeholder={e('batchInput')}
135
+ className='batch-input-holder'
136
+ style={{ cursor: 'pointer' }}
137
+ onClick={this.handleToggle}
138
+ />
139
+ )
140
+ }
141
+
142
+ render () {
143
+ const { open } = this.state
144
+ const { batchInputSelectedTabIds } = this.props
145
145
  const cls = classNames(
146
146
  'batch-input-outer',
147
- {
148
- 'bi-show': open || enter
149
- }
147
+ { 'bi-show': open }
150
148
  )
151
- const inputProps = {
152
- size: 'small',
153
- placeholder: e('batchInput'),
154
- className: 'batch-input-holder'
155
- }
156
149
  const textAreaProps = {
157
- onPressEnter: this.handleEnter,
158
- onClick: this.handleClick,
159
- onBlur: this.handleBlur,
150
+ ref: this.textAreaRef,
151
+ onPressEnter: this.handleSubmit,
152
+ onChange: this.handleChange,
153
+ value: this.state.cmd,
160
154
  size: 'small',
161
- autoSize: { minRows: 1 },
155
+ autoSize: { minRows: 2, maxRows: 6 },
162
156
  placeholder: e('batchInput'),
163
157
  allowClear: true
164
158
  }
@@ -173,22 +167,15 @@ export default class BatchInput extends Component {
173
167
  return (
174
168
  <span
175
169
  className={cls}
176
- onMouseEnter={this.handleMouseEnter}
177
- onMouseLeave={this.handleMouseLeave}
170
+ ref={this.outerRef}
178
171
  >
179
172
  <span className='bi-compact'>
180
- <Input
181
- {...inputProps}
182
- />
173
+ {this.renderTrigger()}
183
174
  </span>
184
175
  <span className='bi-full'>
185
- <AutoComplete
186
- {...opts}
187
- >
188
- <Input.TextArea
189
- {...textAreaProps}
190
- />
191
- </AutoComplete>
176
+ <Input.TextArea
177
+ {...textAreaProps}
178
+ />
192
179
  <TabSelect {...tabSelectProps} />
193
180
  </span>
194
181
  </span>
@@ -1,8 +1,9 @@
1
1
  import { auto } from 'manate/react'
2
2
  import {
3
- Select
3
+ Select,
4
+ Dropdown
4
5
  } from 'antd'
5
- import { InfoCircleOutlined } from '@ant-design/icons'
6
+ import { InfoCircleOutlined, TranslationOutlined } from '@ant-design/icons'
6
7
  import './footer.styl'
7
8
  import { statusMap } from '../../common/constants'
8
9
  import BatchInput from './batch-input'
@@ -54,9 +55,9 @@ export default auto(function FooterEntry (props) {
54
55
  const batchProps = {
55
56
  input: batchInput,
56
57
  tabs: store.tabs,
57
- batchInputs: store.batchInputs,
58
58
  batchInputSelectedTabIds: store.batchInputSelectedTabIds,
59
- activeTabId: store.activeTabId
59
+ activeTabId: store.activeTabId,
60
+ isMobile: store.isMobile
60
61
  }
61
62
  return (
62
63
  <div className='terminal-footer-unit terminal-footer-center'>
@@ -96,6 +97,28 @@ export default auto(function FooterEntry (props) {
96
97
  size: 'small',
97
98
  popupMatchSelectWidth: false
98
99
  }
100
+ if (props.store.isMobile) {
101
+ const items = encodes.map(k => {
102
+ return {
103
+ key: k,
104
+ label: k.toUpperCase(),
105
+ onClick: () => handleSwitchEncoding(k)
106
+ }
107
+ })
108
+ return (
109
+ <div className='terminal-footer-unit terminal-footer-info'>
110
+ <Dropdown
111
+ menu={{ items }}
112
+ placement='topRight'
113
+ trigger={['click']}
114
+ >
115
+ <TranslationOutlined
116
+ className='pointer font18 mobile-encode-trigger'
117
+ />
118
+ </Dropdown>
119
+ </div>
120
+ )
121
+ }
99
122
  return (
100
123
  <div className='terminal-footer-unit terminal-footer-info'>
101
124
  <div className='fleft relative'>
@@ -39,22 +39,26 @@
39
39
  // margin so the history icon stays roughly equidistant from AI and Q
40
40
  margin-left 3px
41
41
  .batch-input-outer
42
+ position relative
42
43
  .bi-full
43
44
  display none
44
45
  &.bi-show
45
- line-height 20px
46
- .bi-full
47
- display inline
46
+ // hide the trigger while the popup is open so we don't show two
47
+ // "batch input" fields at once (desktop trigger is an input-shaped affordance)
48
48
  .bi-compact
49
49
  display none
50
+ .bi-full
51
+ display flex
52
+ flex-direction column
53
+ gap 6px
54
+ position absolute
55
+ bottom 100%
56
+ left 0
57
+ z-index 1000
58
+ background var(--main)
59
+ padding 8px 10px
60
+ box-shadow 0 -2px 8px rgba(0, 0, 0, 0.2)
61
+ border-radius 4px 4px 0 0
62
+ min-width 360px
50
63
  .batch-input-holder
51
- width 100px
52
- @media (max-width: 500px)
53
- .batch-input-outer
54
- &.bi-show
55
- position fixed
56
- left 100px
57
- right 0
58
- bottom 0
59
- z-index 100
60
- background var(--main)
64
+ width 100px
@@ -118,7 +118,8 @@ export default auto(function Layout (props) {
118
118
  'leftSidebarWidth',
119
119
  'addPanelWidth',
120
120
  'pinned',
121
- 'openedSideBar'
121
+ 'openedSideBar',
122
+ 'isMobile'
122
123
  ])
123
124
  }
124
125
  return (
@@ -113,7 +113,9 @@ export default auto(function Index (props) {
113
113
  'not-win': !isWin,
114
114
  'qm-pinned': pinnedQuickCommandBar,
115
115
  fullscreen,
116
- 'is-main': !isSecondInstance
116
+ 'is-main': !isSecondInstance,
117
+ 'is-mobile': store.isMobile,
118
+ 'is-desktop': !store.isMobile
117
119
  })
118
120
  const ext1 = {
119
121
  className: cls
@@ -78,4 +78,27 @@
78
78
  max-width none
79
79
  width 100%
80
80
  .qm-label-select
81
- width 100%
81
+ width 100%
82
+
83
+ // On mobile, stack the quick-command row (name / delay / command / remove)
84
+ // vertically instead of side by side — the horizontal Space.Compact layout
85
+ // is too cramped on narrow screens.
86
+ .is-mobile
87
+ .qm-cmd-row.ant-space-compact
88
+ display flex
89
+ flex-direction column
90
+ align-items stretch
91
+ gap 8px
92
+ .ant-space-compact-item
93
+ width 100%
94
+ // reset the constrained widths so inputs fill the row when stacked
95
+ .qm-name-input
96
+ max-width none
97
+ width 100%
98
+ .ant-input-number
99
+ width 100% !important
100
+ // keep the drag handle + remove button on a tidy line of their own
101
+ > .drag
102
+ align-self flex-start
103
+ > button:last-child
104
+ align-self flex-end
@@ -111,7 +111,7 @@ export default class QuickCommandsItem extends PureComponent {
111
111
  title={this.renderTooltip()}
112
112
  placement='top'
113
113
  mouseEnterDelay={0.5}
114
- overlayClassName='qm-tooltip-overlay'
114
+ classNames={{ root: 'qm-tooltip-overlay' }}
115
115
  >
116
116
  <Button
117
117
  key={id}
@@ -67,7 +67,7 @@ export default function renderQm (form) {
67
67
  return (
68
68
  <Space.Compact
69
69
  align='center'
70
- className='width-100 mg2b'
70
+ className='width-100 mg2b qm-cmd-row'
71
71
  key={field.key}
72
72
  draggable
73
73
  onDragStart={(e) => handleDragStart(e, i)}
@@ -594,6 +594,9 @@ export default class SessionWrapper extends Component {
594
594
  }
595
595
 
596
596
  renderSplitToggle = () => {
597
+ if (window.store.isMobile) {
598
+ return null
599
+ }
597
600
  if (!this.canSplitView() || this.isNotTerminalType()) {
598
601
  return null
599
602
  }
@@ -653,7 +656,8 @@ export default class SessionWrapper extends Component {
653
656
  const simpleMapper = {
654
657
  [paneMap.terminal]: 'T',
655
658
  [paneMap.fileManager]: 'F',
656
- [paneMap.ssh]: 'T'
659
+ [paneMap.ssh]: 'T',
660
+ [paneMap.sftp]: 'S'
657
661
  }
658
662
  return (
659
663
  <div className='term-sftp-tabs fleft'>
@@ -673,8 +677,8 @@ export default class SessionWrapper extends Component {
673
677
  onClick={() => this.onChangePane(types[i])}
674
678
  >
675
679
  <span className='type-tab-txt'>
676
- <span className='w500'>{e(type)}</span>
677
- <span className='l500'>{simpleMapper[type]}</span>
680
+ <span className='w800'>{e(type)}</span>
681
+ <span className='l800'>{simpleMapper[type]}</span>
678
682
  <span className='type-tab-line' />
679
683
  </span>
680
684
  </span>
@@ -110,7 +110,7 @@ export default function DeepLinkControl () {
110
110
  <Tooltip
111
111
  title={renderTooltipContent()}
112
112
  >
113
- <Space>
113
+ <div className='deep-link-btns'>
114
114
  {
115
115
  !isAllRegistered && (
116
116
  <Button
@@ -134,7 +134,7 @@ export default function DeepLinkControl () {
134
134
  </Button>
135
135
  )
136
136
  }
137
- </Space>
137
+ </div>
138
138
  </Tooltip>
139
139
  </div>
140
140
  )
@@ -182,6 +182,7 @@ export default auto(function SettingModalWrap (props) {
182
182
  visible={show}
183
183
  useSystemTitleBar={useSystemTitleBar}
184
184
  innerWidth={innerWidth}
185
+ isMobile={props.store.isMobile}
185
186
  >
186
187
  {renderTabs()}
187
188
  </SettingModal>
@@ -11,9 +11,9 @@ import {
11
11
  } from '@ant-design/icons'
12
12
  import {
13
13
  Button,
14
+ Pagination,
14
15
  Modal,
15
16
  Space,
16
- Table,
17
17
  Tag,
18
18
  Tooltip,
19
19
  Typography,
@@ -126,8 +126,10 @@ export default class SettingPasswords extends Component {
126
126
  })
127
127
  }
128
128
 
129
- handleTableChange = (pagination) => {
130
- this.setState({ pagination })
129
+ handlePageChange = (page, pageSize) => {
130
+ this.setState({
131
+ pagination: { current: page, pageSize }
132
+ })
131
133
  }
132
134
 
133
135
  getFilteredData = () => {
@@ -143,88 +145,48 @@ export default class SettingPasswords extends Component {
143
145
  })
144
146
  }
145
147
 
146
- getColumns = () => {
147
- const columns = [
148
- {
149
- title: e('password'),
150
- dataIndex: 'password',
151
- key: 'password',
152
- render: () => {
153
- const props0 = {
154
- children: [
155
- <KeyOutlined key='icon' />,
156
- <TextAnt keyboard key='text'>********</TextAnt>
157
- ]
158
- }
159
- return <Space>{props0.children}</Space>
160
- }
161
- },
162
- {
163
- title: e('count'),
164
- dataIndex: 'count',
165
- key: 'count',
166
- width: 80,
167
- render: (count) => {
168
- const props0 = {
169
- color: 'blue',
170
- children: count
171
- }
172
- return <Tag {...props0} />
173
- }
174
- },
175
- {
176
- title: e('host'),
177
- dataIndex: 'titles',
178
- key: 'host',
179
- render: (titles) => {
180
- const display = titles.length > 2
181
- ? `${titles.slice(0, 2).join(', ')}... (+${titles.length - 2})`
182
- : titles.join(', ')
183
- const props0 = {
184
- title: titles.join('\n'),
185
- children: <span>{display}</span>
186
- }
187
- return <Tooltip {...props0} />
188
- }
189
- },
190
- {
191
- title: e('actions'),
192
- key: 'actions',
193
- width: 80,
194
- render: (_, record) => {
195
- const copyProps0 = {
196
- type: 'text',
197
- icon: <CopyOutlined />,
198
- onClick: () => this.handleCopyPassword(record.password)
199
- }
200
- const editProps0 = {
201
- type: 'text',
202
- icon: <EditOutlined />,
203
- onClick: () => this.showEditModal(record)
204
- }
205
- const copyTooltipProps = {
206
- title: e('copy'),
207
- children: <Button {...copyProps0} />
208
- }
209
- const editTooltipProps = {
210
- title: e('changePassword'),
211
- children: <Button {...editProps0} />
212
- }
213
- const spaceProps0 = {
214
- children: [
215
- <Tooltip key='copy' {...copyTooltipProps} />,
216
- <Tooltip key='edit' {...editTooltipProps} />
217
- ]
218
- }
219
- return <Space>{spaceProps0.children}</Space>
220
- }
221
- }
222
- ]
223
- return columns
148
+ renderItem = (record) => {
149
+ const { password, count, titles } = record
150
+ const display = titles.length > 2
151
+ ? `${titles.slice(0, 2).join(', ')}... (+${titles.length - 2})`
152
+ : titles.join(', ')
153
+ const copyProps = {
154
+ type: 'text',
155
+ icon: <CopyOutlined />,
156
+ onClick: () => this.handleCopyPassword(password)
157
+ }
158
+ const editProps = {
159
+ type: 'text',
160
+ icon: <EditOutlined />,
161
+ onClick: () => this.showEditModal(record)
162
+ }
163
+ return (
164
+ <div className='setting-passwords-item' key={password}>
165
+ <div className='setting-passwords-item-inner'>
166
+ <Space className='setting-passwords-pwd-cell'>
167
+ <KeyOutlined />
168
+ <TextAnt keyboard>********</TextAnt>
169
+ </Space>
170
+ <Tag color='blue' className='setting-passwords-count-cell'>{count}</Tag>
171
+ <Tooltip title={titles.join('\n')}>
172
+ <span className='setting-passwords-host-cell'>{display}</span>
173
+ </Tooltip>
174
+ <Space className='setting-passwords-actions-cell'>
175
+ <Tooltip title={e('copy')}>
176
+ <Button {...copyProps} />
177
+ </Tooltip>
178
+ <Tooltip title={e('changePassword')}>
179
+ <Button {...editProps} />
180
+ </Tooltip>
181
+ </Space>
182
+ </div>
183
+ </div>
184
+ )
224
185
  }
225
186
 
226
187
  renderContent () {
227
188
  const { search, pagination } = this.state
189
+ const { current, pageSize } = pagination
228
190
  const data = this.getFilteredData()
229
191
 
230
192
  if (data.length === 0) {
@@ -241,19 +203,29 @@ export default class SettingPasswords extends Component {
241
203
  onChange: this.handleSearchChange,
242
204
  placeholder: e('search')
243
205
  }
244
- const tableProps0 = {
245
- dataSource: data,
246
- columns: this.getColumns(),
247
- rowKey: 'password',
248
- pagination: { ...pagination, showSizeChanger: true },
249
- onChange: this.handleTableChange,
206
+
207
+ // clamp current page when filtering shrinks the data set
208
+ const pageCount = Math.ceil(data.length / pageSize)
209
+ const safePage = Math.min(current, Math.max(pageCount, 1))
210
+ const pagedData = data.slice((safePage - 1) * pageSize, safePage * pageSize)
211
+
212
+ const paginationProps0 = {
213
+ total: data.length,
214
+ current: safePage,
215
+ pageSize,
216
+ showSizeChanger: true,
217
+ onChange: this.handlePageChange,
218
+ onShowSizeChange: this.handlePageChange,
250
219
  size: 'small'
251
220
  }
252
221
 
253
222
  return (
254
223
  <div>
255
224
  <Search {...searchProps0} />
256
- <Table {...tableProps0} />
225
+ <div className='setting-passwords-list'>
226
+ {pagedData.map(item => this.renderItem(item))}
227
+ </div>
228
+ <Pagination {...paginationProps0} />
257
229
  </div>
258
230
  )
259
231
  }
@@ -16,6 +16,18 @@ export default class SettingWrap extends Component {
16
16
  )
17
17
  }
18
18
 
19
+ renderRightClose () {
20
+ if (this.props.isMobile) {
21
+ return null
22
+ }
23
+ return (
24
+ <CloseCircleOutlined
25
+ className='close-setting-wrap-icon close-setting-wrap'
26
+ onClick={this.props.onCancel}
27
+ />
28
+ )
29
+ }
30
+
19
31
  render () {
20
32
  const pops = {
21
33
  open: this.props.visible,
@@ -29,10 +41,7 @@ export default class SettingWrap extends Component {
29
41
  <Drawer
30
42
  {...pops}
31
43
  >
32
- <CloseCircleOutlined
33
- className='close-setting-wrap-icon close-setting-wrap'
34
- onClick={this.props.onCancel}
35
- />
44
+ {this.renderRightClose()}
36
45
  <CloseCircleOutlined
37
46
  className='close-setting-wrap alt-close-setting-wrap'
38
47
  onClick={this.props.onCancel}
@@ -10,4 +10,40 @@
10
10
  left 43px
11
11
  height 60px
12
12
  right 0
13
- z-index 888
13
+ z-index 888
14
+ .deep-link-btns
15
+ display flex
16
+ flex-wrap wrap
17
+ gap 8px
18
+
19
+ // passwords list — replaces the old table so content wraps
20
+ // instead of forcing horizontal scroll on narrow/mobile screens
21
+ .setting-passwords-list
22
+ border 1px solid var(--main-light)
23
+ border-radius 6px
24
+ overflow hidden
25
+ margin 8px 0
26
+ .setting-passwords-item
27
+ padding 8px 12px
28
+ &:not(:last-child)
29
+ border-bottom 1px solid var(--main-light)
30
+ .setting-passwords-item
31
+ .setting-passwords-item-inner
32
+ display flex
33
+ flex-wrap wrap
34
+ align-items center
35
+ gap 8px
36
+ width 100%
37
+ .setting-passwords-pwd-cell
38
+ flex-shrink 0
39
+ .setting-passwords-count-cell
40
+ flex-shrink 0
41
+ margin 0
42
+ .setting-passwords-host-cell
43
+ flex 1 1 120px
44
+ min-width 120px
45
+ word-break break-all
46
+ line-height 1.5
47
+ .setting-passwords-actions-cell
48
+ flex-shrink 0
49
+ margin-left auto
@@ -65,7 +65,7 @@ export default function DataTransport (props) {
65
65
  }
66
66
 
67
67
  return (
68
- <div className='pd2 fix'>
68
+ <div className='pd2 fix sync-data-transport'>
69
69
  <div className='fleft'>
70
70
  <Button
71
71
  icon={<ExportOutlined />}
@@ -9,6 +9,7 @@ import { Tabs, Tooltip } from 'antd'
9
9
  import MultiSelectModal from '../common/multi-select-modal'
10
10
  import {
11
11
  ArrowsAltOutlined,
12
+ CloseOutlined,
12
13
  EditOutlined,
13
14
  PlusCircleOutlined,
14
15
  ShrinkOutlined,
@@ -105,6 +106,10 @@ export default memo(function SidebarPanel (props) {
105
106
  onClick={store.handlePin}
106
107
  />
107
108
  </Tooltip>
109
+ <CloseOutlined
110
+ {...prps}
111
+ onClick={store.handleCloseSidebar}
112
+ />
108
113
  </div>
109
114
  <div className='pd1y pd2x'>
110
115
  <Tabs {...tabsProps} />
@@ -37,7 +37,7 @@ export default memo(function TransferList (props) {
37
37
  const popProps = {
38
38
  placement: 'right',
39
39
  destroyOnHidden: true,
40
- overlayClassName: 'transfer-list-card',
40
+ classNames: { root: 'transfer-list-card' },
41
41
  content: <TransferModal {...transferModalProps} />
42
42
  }
43
43
  return (
@@ -83,6 +83,9 @@ export default class Tabs extends Component {
83
83
  }
84
84
 
85
85
  isOverflow = () => {
86
+ if (this.props.isMobile || !this.domRef.current) {
87
+ return false
88
+ }
86
89
  const { tabs = [] } = this.props
87
90
  const len = tabs.length
88
91
  const addBtnWidth = 22
@@ -120,6 +123,9 @@ export default class Tabs extends Component {
120
123
  }
121
124
 
122
125
  adjustScroll = () => {
126
+ if (this.props.isMobile || !this.domRef.current) {
127
+ return
128
+ }
123
129
  const { tabs, currentBatchTabId, batch } = this.props
124
130
  const index = tabs.findIndex(t => t.id === currentBatchTabId)
125
131
  const tabsDomWith = Array.from(
@@ -139,6 +145,9 @@ export default class Tabs extends Component {
139
145
  }
140
146
 
141
147
  handleScrollLeft = () => {
148
+ if (!this.domRef.current) {
149
+ return
150
+ }
142
151
  let { scrollLeft } = this.domRef.current
143
152
  scrollLeft = scrollLeft - tabMargin - tabWidth
144
153
  if (scrollLeft < 0) {
@@ -148,6 +157,9 @@ export default class Tabs extends Component {
148
157
  }
149
158
 
150
159
  handleScrollRight = () => {
160
+ if (!this.domRef.current) {
161
+ return
162
+ }
151
163
  let { scrollLeft } = this.domRef.current
152
164
  scrollLeft = scrollLeft + tabMargin + tabWidth
153
165
  if (scrollLeft < 0) {
@@ -349,8 +361,69 @@ export default class Tabs extends Component {
349
361
  return null
350
362
  }
351
363
 
364
+ renderMobileTabsDropdown (items) {
365
+ return (
366
+ <Dropdown
367
+ menu={{ items }}
368
+ placement='bottomRight'
369
+ trigger={['click']}
370
+ >
371
+ <DownOutlined className='tabs-dd-icon' />
372
+ </Dropdown>
373
+ )
374
+ }
375
+
376
+ renderMobileTabsExtra (items) {
377
+ return (
378
+ <div className='mobile-tabs-extra'>
379
+ {this.renderAddBtn()}
380
+ {
381
+ items && items.length
382
+ ? this.renderMobileTabsDropdown(items)
383
+ : null
384
+ }
385
+ </div>
386
+ )
387
+ }
388
+
389
+ renderMobileTabs () {
390
+ const { tabs = [], currentBatchTabId } = this.props
391
+ const currentTab = tabs.find(t => t.id === currentBatchTabId)
392
+ const items = tabs.map((t, i) => {
393
+ return {
394
+ key: i + '##' + t.id,
395
+ label: (
396
+ <span><TabTitle tab={t} /></span>
397
+ ),
398
+ onClick: () => this.handleClickMenu({ key: i + '##' + t.id })
399
+ }
400
+ })
401
+ const tabProps = {
402
+ ...this.props,
403
+ tab: currentTab,
404
+ isLast: true,
405
+ addTab: this.handleTabAdd,
406
+ tabIndex: 0
407
+ }
408
+ const currentTabEl = currentTab
409
+ ? <Tab {...tabProps} key={currentTab.id} />
410
+ : null
411
+ return (
412
+ <div className='tabs mobile-tabs' ref={this.tabsRef}>
413
+ <div className='tabs-inner mobile-tabs-inner'>
414
+ {currentTabEl}
415
+ </div>
416
+ {this.renderMobileTabsExtra(items)}
417
+ {this.renderWindowControl()}
418
+ </div>
419
+ )
420
+ }
421
+
352
422
  renderTabs () {
353
423
  const { overflow } = this.state
424
+ if (this.props.isMobile) {
425
+ return this.renderMobileTabs()
426
+ }
354
427
  return (
355
428
  <div className='tabs' ref={this.tabsRef}>
356
429
  {this.renderContent()}
@@ -1,8 +1,134 @@
1
- .l500
1
+ .l800
2
2
  display none !important
3
3
 
4
- @media (max-width: 500px)
5
- .w500
4
+ @media (max-width: 800px)
5
+ .w800
6
6
  display none !important
7
- .l500
8
- display inline !important
7
+ .l800
8
+ display inline !important
9
+
10
+ // ---------------------------------------------------------------------------
11
+ // Mobile UI driven by store.isMobile (is-mobile class on root)
12
+ // Breakpoint: 800px (see mobileBreakpoint in constants.js)
13
+ // ---------------------------------------------------------------------------
14
+ .is-mobile
15
+ // ---- 1. Tabs: show only current tab + dropdown, close icon always visible
16
+ .tabs.mobile-tabs
17
+ display flex
18
+ align-items center
19
+ overflow hidden
20
+ // leave room for window controls on desktop platforms resized narrow
21
+ &.not-system-ui.not-mac.not-webapp
22
+ .tabs.mobile-tabs
23
+ padding-right 94px
24
+ .mobile-tabs-inner
25
+ flex 1
26
+ min-width 0
27
+ overflow hidden
28
+ display flex
29
+ align-items center
30
+ margin-left 0 !important
31
+ width auto !important
32
+ .tab
33
+ flex 1
34
+ max-width none
35
+ min-width 0
36
+ width auto
37
+ text-align left
38
+ // close icon always visible on mobile
39
+ .tab-close
40
+ display block
41
+ .mobile-tabs-extra
42
+ flex-shrink 0
43
+ display flex
44
+ align-items center
45
+ padding 0 10px
46
+ line-height 36px
47
+ z-index 20
48
+ .tabs-add-btn
49
+ margin 0
50
+ &.empty
51
+ font-size 20px
52
+ margin 0
53
+ .tabs-dd-icon
54
+ margin-left 8px
55
+ font-size 14px
56
+ color var(--text)
57
+ &:hover
58
+ color var(--text-light)
59
+ // tabs-extra must not render on mobile (defensive css too)
60
+ .tabs-extra
61
+ display none !important
62
+
63
+ // ---- 2. Footer batch input: show "B" button, click to expand
64
+ // match the quick-commands "Q" button: no flex-grow, same small margin
65
+ .terminal-footer-center
66
+ flex-grow 0
67
+ margin-left 3px
68
+ .batch-input-outer
69
+ &.bi-show
70
+ .bi-full
71
+ // full-width fixed popup above the footer (mobile is narrow)
72
+ position fixed
73
+ left 0
74
+ right 0
75
+ bottom 36px
76
+ z-index 200
77
+ background var(--main)
78
+ padding 8px 10px
79
+ box-shadow 0 -2px 8px rgba(0, 0, 0, 0.2)
80
+ width auto
81
+ min-width 0
82
+ border-radius 0
83
+
84
+ // ---- 3. Footer encode select: show icon, click to open dropdown
85
+ // match antd Button small icon size (~14px) used by sibling icons
86
+ .mobile-encode-trigger,
87
+ .terminal-info-icon
88
+ font-size 14px !important
89
+ line-height 36px
90
+ padding 0 4px
91
+ &:hover
92
+ color var(--text-light)
93
+
94
+ // ---- 4. Modal: full width / full bleed on mobile
95
+ .custom-modal-container
96
+ padding 0
97
+ .custom-modal-content
98
+ width 100% !important
99
+ max-width 100vw
100
+ margin-top 0
101
+ min-height 100vh
102
+ border-radius 0
103
+
104
+ // ---- 5. Sync panel data transport: stack export/import and auto-sync
105
+ // vertically with a gap. On narrow screens the right-floated
106
+ // auto-sync block wraps under the import button with no vertical
107
+ // gap — left-align it and add breathing room.
108
+ .sync-data-transport
109
+ .fleft,
110
+ .fright
111
+ float none
112
+ .fright
113
+ margin-top 8px
114
+ // the switch carries mg3l (32px left margin) to separate it from the
115
+ // import button on desktop — reset it when stacked so it left-aligns
116
+ .ant-switch
117
+ margin-left 0
118
+
119
+ // ---- 6. Solid Radio.Button groups (e.g. SSH auth type selector, session
120
+ // type selector): on narrow screens the connected button row wraps,
121
+ // but antd styles it as a single horizontal strip — adjacent buttons
122
+ // share borders via -1px margin and border-radius is only applied to
123
+ // the first/last child. When the row wraps, radius/borders break and
124
+ // the UI looks messed up. Convert each button to a standalone pill so
125
+ // wrapping stays tidy.
126
+ .ant-radio-group.ant-radio-group-solid
127
+ display flex
128
+ flex-wrap wrap
129
+ gap 6px
130
+ .ant-radio-button-wrapper
131
+ border-radius 3px
132
+ margin-inline-end 0
133
+ &::before
134
+ display none
@@ -11,7 +11,9 @@ import {
11
11
  addPanelWidthLsKey,
12
12
  dismissDelKeyTipLsKey,
13
13
  connectionMap,
14
- lastAiChatSessionIdKey
14
+ lastAiChatSessionIdKey,
15
+ mobileBreakpoint,
16
+ splitMap
15
17
  } from '../common/constants'
16
18
  import * as ls from '../common/safe-local-storage'
17
19
  import { refs, refsStatic } from '../components/common/ref'
@@ -58,14 +60,21 @@ export default Store => {
58
60
  Store.prototype.onResize = debounce(async function () {
59
61
  const { width, height } = await window.pre.runGlobalAsync('getScreenSize')
60
62
  const isMaximized = window.pre.runSync('isMaximized')
63
+ const w = window.innerWidth
64
+ const isMobile = w <= mobileBreakpoint
61
65
  const update = {
62
66
  height: window.innerHeight,
63
- innerWidth: window.innerWidth,
67
+ innerWidth: w,
64
68
  screenWidth: width,
65
69
  screenHeight: height,
66
- isMaximized
70
+ isMaximized,
71
+ isMobile
67
72
  }
68
73
  window.store.storeAssign(update)
74
+ // Force single-column layout on mobile
75
+ if (isMobile && window.store.layout !== splitMap.c1) {
76
+ window.store.setLayout(splitMap.c1)
77
+ }
69
78
  window.pre.runGlobalAsync('setWindowSize', {
70
79
  ...update,
71
80
  height: window.outerHeight
@@ -23,7 +23,8 @@ import {
23
23
  resolutionsLsKey,
24
24
  syncServerDataKey,
25
25
  splitMap,
26
- lastAiChatSessionIdKey
26
+ lastAiChatSessionIdKey,
27
+ mobileBreakpoint
27
28
  } from '../common/constants'
28
29
  import * as ls from '../common/safe-local-storage'
29
30
  import { exclude } from 'manate'
@@ -200,6 +201,7 @@ export default () => {
200
201
  height: 500,
201
202
  isMaximized: window.pre.runSync('isMaximized'),
202
203
  hasNodePty: window.pre.runSync('nodePtyCheck'),
204
+ isMobile: window.innerWidth <= mobileBreakpoint,
203
205
  fullscreen: false,
204
206
  hideDelKeyTip: ls.getItem(dismissDelKeyTipLsKey) === 'y',
205
207
  tabsHeight: 36,
@@ -29,6 +29,15 @@ export default Store => {
29
29
  }
30
30
  }
31
31
 
32
+ Store.prototype.handleCloseSidebar = function () {
33
+ const { store } = window
34
+ if (store.pinned) {
35
+ ls.setItem(sidebarPinnedKey, 'false')
36
+ store.pinned = false
37
+ }
38
+ store.setOpenedSideBar('')
39
+ }
40
+
32
41
  Store.prototype.handleSidebarPanelTab = function (tab) {
33
42
  window.store.sidebarPanelTab = tab
34
43
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@electerm/electerm-react",
3
- "version": "3.15.152",
3
+ "version": "3.15.155",
4
4
  "description": "react components src for electerm",
5
5
  "main": "./client/components/main/main.jsx",
6
6
  "license": "MIT",