@electerm/electerm-react 5.0.8 → 5.1.6

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 (39) hide show
  1. package/client/common/constants.js +2 -0
  2. package/client/common/default-setting.js +10 -1
  3. package/client/common/left-sidebar-icon-defs.js +36 -0
  4. package/client/components/ai/ai-config-props.js +8 -0
  5. package/client/components/bookmark-form/common/fields.jsx +3 -2
  6. package/client/components/bookmark-form/common/ssh-agent.jsx +3 -2
  7. package/client/components/bookmark-form/common/x11.jsx +2 -2
  8. package/client/components/common/switch.jsx +63 -0
  9. package/client/components/common/switch.styl +7 -0
  10. package/client/components/footer/cmd-history.jsx +4 -8
  11. package/client/components/layout/layout.jsx +8 -5
  12. package/client/components/main/main.jsx +7 -2
  13. package/client/components/profile/profile-form-elem.jsx +2 -2
  14. package/client/components/quick-commands/quick-commands-form-elem.jsx +2 -2
  15. package/client/components/rdp/rdp-session.jsx +3 -4
  16. package/client/components/setting-panel/setting-common.jsx +9 -5
  17. package/client/components/setting-panel/setting-left-sidebar-icons.jsx +54 -0
  18. package/client/components/setting-panel/setting-terminal.jsx +3 -4
  19. package/client/components/setting-sync/data-import.jsx +3 -4
  20. package/client/components/setting-sync/setting-sync-form.jsx +3 -2
  21. package/client/components/sftp/file-compare-modal.jsx +1 -1
  22. package/client/components/sidebar/history.jsx +3 -8
  23. package/client/components/sidebar/index.jsx +22 -71
  24. package/client/components/sidebar/left-sidebar-icons.jsx +148 -0
  25. package/client/components/sidebar/transfer-list.jsx +1 -1
  26. package/client/components/spice/spice-session.jsx +4 -5
  27. package/client/components/terminal/shortcut-bar-defs.js +131 -0
  28. package/client/components/terminal/shortcut-bar-edit.jsx +205 -0
  29. package/client/components/terminal/shortcut-bar-entry.jsx +34 -0
  30. package/client/components/terminal/shortcut-bar.jsx +150 -0
  31. package/client/components/terminal/shortcut-bar.styl +186 -0
  32. package/client/components/terminal/terminal-color-query.mjs +47 -0
  33. package/client/components/terminal/terminal.jsx +39 -1
  34. package/client/components/terminal/terminal.styl +8 -0
  35. package/client/components/terminal-info/base.jsx +5 -7
  36. package/client/components/widgets/widget-form.jsx +3 -2
  37. package/client/store/init-state.js +3 -0
  38. package/client/store/mcp-handler.js +29 -1
  39. package/package.json +1 -1
@@ -1,27 +1,20 @@
1
1
  import {
2
- BookOutlined,
3
- CloudSyncOutlined,
4
2
  InfoCircleOutlined,
5
- PictureOutlined,
6
- PlusCircleOutlined,
7
- SettingOutlined,
8
3
  UpCircleOutlined,
9
- AppstoreOutlined,
10
- ThunderboltOutlined,
11
4
  AimOutlined,
12
5
  MenuFoldOutlined
13
6
  } from '@ant-design/icons'
14
- import { Tooltip, Popover } from 'antd'
7
+ import { Tooltip } from 'antd'
15
8
  import SideBarPanel from './sidebar-panel'
16
- import TransferList from './transfer-list'
17
9
  import MenuBtn from '../sys-menu/menu-btn'
18
- import QuickConnect from '../tabs/quick-connect'
19
10
  import {
20
11
  settingMap,
21
12
  modals
22
13
  } from '../../common/constants'
23
14
  import SideIcon from './side-icon'
24
15
  import SidePanel from './side-panel'
16
+ import LeftSidebarIcons from './left-sidebar-icons'
17
+ import TransferList from './transfer-list'
25
18
  import hasActiveInput from '../../common/has-active-input'
26
19
  import './sidebar.styl'
27
20
 
@@ -47,7 +40,9 @@ export default function Sidebar (props) {
47
40
  showInfoModal,
48
41
  sidebarPanelTab,
49
42
  openWidgetsModal,
50
- zoom
43
+ zoom,
44
+ leftSideBarIcons,
45
+ widgetInstancesLength
51
46
  } = props
52
47
 
53
48
  const { store } = window
@@ -146,67 +141,23 @@ export default function Sidebar (props) {
146
141
  <div className='control-icon-wrap'>
147
142
  <MenuBtn store={store} config={store.config} />
148
143
  </div>
149
- <SideIcon
150
- title={e('newBookmark')}
151
- >
152
- <PlusCircleOutlined
153
- className='font22 iblock control-icon'
154
- onClick={onNewSsh}
155
- />
156
- </SideIcon>
157
- <Popover
158
- content={<QuickConnect inputOnly />}
159
- trigger='click'
160
- placement='right'
161
- >
162
- <div className='control-icon-wrap' title={e('quickConnect')}>
163
- <ThunderboltOutlined
164
- className='font20 iblock control-icon'
165
- />
166
- </div>
167
- </Popover>
168
- <SideIcon
169
- title={e(settingMap.bookmarks)}
170
- active={bookmarksActive}
171
- >
172
- <BookOutlined
173
- onClick={handleClickBookmark}
174
- className='font20 iblock control-icon'
175
- />
176
- </SideIcon>
144
+ <LeftSidebarIcons
145
+ iconIds={leftSideBarIcons}
146
+ handleClickBookmark={handleClickBookmark}
147
+ onNewSsh={onNewSsh}
148
+ openSetting={openSetting}
149
+ openSettingSync={openSettingSync}
150
+ openTerminalThemes={openTerminalThemes}
151
+ openWidgetsModal={openWidgetsModal}
152
+ bookmarksActive={bookmarksActive}
153
+ themeActive={themeActive}
154
+ settingActive={settingActive}
155
+ syncActive={syncActive}
156
+ widgetsActive={widgetsActive}
157
+ isSyncingSetting={isSyncingSetting}
158
+ widgetInstancesLength={widgetInstancesLength}
159
+ />
177
160
  <TransferList {...transferProps} />
178
- <SideIcon
179
- title={e(settingMap.terminalThemes)}
180
- active={themeActive}
181
- >
182
- <PictureOutlined
183
- className='font20 iblock pointer control-icon'
184
- onClick={openTerminalThemes}
185
- />
186
- </SideIcon>
187
- <SideIcon
188
- title={e(settingMap.setting)}
189
- active={settingActive}
190
- >
191
- <SettingOutlined className='iblock font20 control-icon' onClick={openSetting} />
192
- </SideIcon>
193
- <SideIcon
194
- title={e('settingSync')}
195
- active={syncActive}
196
- >
197
- <CloudSyncOutlined
198
- className='iblock font20 control-icon'
199
- onClick={openSettingSync}
200
- spin={isSyncingSetting}
201
- />
202
- </SideIcon>
203
- <SideIcon
204
- title={e('widgets')}
205
- active={widgetsActive}
206
- >
207
- <AppstoreOutlined className='iblock font20 control-icon' onClick={openWidgetsModal} />
208
- </SideIcon>
209
-
210
161
  <SideIcon
211
162
  title={e('about')}
212
163
  active={showInfoModal}
@@ -0,0 +1,148 @@
1
+ /**
2
+ * renders the customizable far-left sidebar icons.
3
+ *
4
+ * which icons appear, and in what order, is driven by `iconIds`
5
+ * (from config.leftSideBarIcons). The "about", "hide sidebar" and
6
+ * "file transfer" icons are not customizable — they are rendered
7
+ * directly by the parent (sidebar/index.jsx).
8
+ */
9
+
10
+ import {
11
+ AppstoreOutlined,
12
+ BookOutlined,
13
+ CloudSyncOutlined,
14
+ PictureOutlined,
15
+ PlusCircleOutlined,
16
+ SettingOutlined,
17
+ ThunderboltOutlined
18
+ } from '@ant-design/icons'
19
+ import { Badge, Popover } from 'antd'
20
+ import SideIcon from './side-icon'
21
+ import QuickConnect from '../tabs/quick-connect'
22
+ import { settingMap } from '../../common/constants'
23
+ import { defaultLeftSideBarIcons } from '../../common/left-sidebar-icon-defs'
24
+
25
+ const e = window.translate
26
+
27
+ export default function LeftSidebarIcons (props) {
28
+ const {
29
+ iconIds,
30
+ handleClickBookmark,
31
+ onNewSsh,
32
+ openSetting,
33
+ openSettingSync,
34
+ openTerminalThemes,
35
+ openWidgetsModal,
36
+ bookmarksActive,
37
+ themeActive,
38
+ settingActive,
39
+ syncActive,
40
+ widgetsActive,
41
+ isSyncingSetting,
42
+ widgetInstancesLength
43
+ } = props
44
+
45
+ const ids = Array.isArray(iconIds) && iconIds.length
46
+ ? iconIds
47
+ : defaultLeftSideBarIcons
48
+
49
+ const registry = {
50
+ newBookmark: (
51
+ <SideIcon
52
+ key='newBookmark'
53
+ title={e('newBookmark')}
54
+ >
55
+ <PlusCircleOutlined
56
+ className='font22 iblock control-icon'
57
+ onClick={onNewSsh}
58
+ />
59
+ </SideIcon>
60
+ ),
61
+ quickConnect: (
62
+ <Popover
63
+ key='quickConnect'
64
+ content={<QuickConnect inputOnly />}
65
+ trigger='click'
66
+ placement='right'
67
+ >
68
+ <div
69
+ className='control-icon-wrap'
70
+ title={e('quickConnect')}
71
+ >
72
+ <ThunderboltOutlined className='font20 iblock control-icon' />
73
+ </div>
74
+ </Popover>
75
+ ),
76
+ bookmarks: (
77
+ <SideIcon
78
+ key='bookmarks'
79
+ title={e(settingMap.bookmarks)}
80
+ active={bookmarksActive}
81
+ >
82
+ <BookOutlined
83
+ onClick={handleClickBookmark}
84
+ className='font20 iblock control-icon'
85
+ />
86
+ </SideIcon>
87
+ ),
88
+ terminalThemes: (
89
+ <SideIcon
90
+ key='terminalThemes'
91
+ title={e(settingMap.terminalThemes)}
92
+ active={themeActive}
93
+ >
94
+ <PictureOutlined
95
+ className='font20 iblock pointer control-icon'
96
+ onClick={openTerminalThemes}
97
+ />
98
+ </SideIcon>
99
+ ),
100
+ setting: (
101
+ <SideIcon
102
+ key='setting'
103
+ title={e(settingMap.setting)}
104
+ active={settingActive}
105
+ >
106
+ <SettingOutlined
107
+ className='iblock font20 control-icon'
108
+ onClick={openSetting}
109
+ />
110
+ </SideIcon>
111
+ ),
112
+ settingSync: (
113
+ <SideIcon
114
+ key='settingSync'
115
+ title={e('settingSync')}
116
+ active={syncActive}
117
+ >
118
+ <CloudSyncOutlined
119
+ className='iblock font20 control-icon'
120
+ onClick={openSettingSync}
121
+ spin={isSyncingSetting}
122
+ />
123
+ </SideIcon>
124
+ ),
125
+ widgets: (
126
+ <SideIcon
127
+ key='widgets'
128
+ title={e('widgets')}
129
+ active={widgetsActive}
130
+ >
131
+ <Badge
132
+ count={widgetInstancesLength}
133
+ size='small'
134
+ offset={[0, 0]}
135
+ color='green'
136
+ overflowCount={99}
137
+ >
138
+ <AppstoreOutlined
139
+ className='iblock font20 control-icon'
140
+ onClick={openWidgetsModal}
141
+ />
142
+ </Badge>
143
+ </SideIcon>
144
+ )
145
+ }
146
+
147
+ return ids.map(id => registry[id] || null)
148
+ }
@@ -25,7 +25,7 @@ export default memo(function TransferList (props) {
25
25
  const bdProps = {
26
26
  count: len,
27
27
  size: 'small',
28
- offset: [-10, -5],
28
+ offset: [0, 0],
29
29
  color,
30
30
  overflowCount: 99
31
31
  }
@@ -10,11 +10,11 @@ import {
10
10
  ReloadOutlined
11
11
  } from '@ant-design/icons'
12
12
  import {
13
- Spin,
14
- Switch
13
+ Spin
15
14
  } from 'antd'
16
15
  import * as ls from '../../common/safe-local-storage'
17
16
  import RemoteFloatControl from '../common/remote-float-control'
17
+ import SwitchLabel from '../common/switch'
18
18
  import './spice.styl'
19
19
 
20
20
  async function loadSpiceModule () {
@@ -132,8 +132,7 @@ export default class SpiceSession extends PureComponent {
132
132
  const scaleProps = {
133
133
  checked: this.state.scaleViewport,
134
134
  onChange: this.handleScaleViewChange,
135
- unCheckedChildren: window.translate('scaleViewport'),
136
- checkedChildren: window.translate('scaleViewport'),
135
+ label: window.translate('scaleViewport'),
137
136
  className: 'mg1l'
138
137
  }
139
138
  return (
@@ -144,7 +143,7 @@ export default class SpiceSession extends PureComponent {
144
143
  className='mg2r mg1l pointer'
145
144
  />
146
145
  {this.renderInfo()}
147
- <Switch
146
+ <SwitchLabel
148
147
  {...scaleProps}
149
148
  />
150
149
  </div>
@@ -0,0 +1,131 @@
1
+ /**
2
+ * shortcut-bar data: the candidate button library, the default selection, and
3
+ * localStorage persistence. Pure data (no React) so it can be shared by the
4
+ * bar (shortcut-bar.jsx) and the edit modal (shortcut-bar-edit.jsx).
5
+ */
6
+
7
+ import * as ls from '../../common/safe-local-storage'
8
+ import { shortcutBarLsKey } from '../../common/constants'
9
+
10
+ // build control bytes at runtime — keeps source free of literal control chars
11
+ const ESC = String.fromCharCode(0x1b)
12
+ const DEL = String.fromCharCode(0x7f)
13
+
14
+ // --- candidate library -----------------------------------------------------
15
+
16
+ const ctrlChars = 'abcdefghijklmnopqrstuvwxyz'.split('')
17
+ const ctrlButtons = ctrlChars.map(ch => ({
18
+ id: 'ctrl+' + ch,
19
+ label: 'Ctrl+' + ch.toUpperCase(),
20
+ data: String.fromCharCode(ch.charCodeAt(0) - 96)
21
+ }))
22
+
23
+ const fKeys = [
24
+ ['f1', 'F1', ESC + 'OP'],
25
+ ['f2', 'F2', ESC + 'OQ'],
26
+ ['f3', 'F3', ESC + 'OR'],
27
+ ['f4', 'F4', ESC + 'OS'],
28
+ ['f5', 'F5', ESC + '[15~'],
29
+ ['f6', 'F6', ESC + '[17~'],
30
+ ['f7', 'F7', ESC + '[18~'],
31
+ ['f8', 'F8', ESC + '[19~'],
32
+ ['f9', 'F9', ESC + '[20~'],
33
+ ['f10', 'F10', ESC + '[21~'],
34
+ ['f11', 'F11', ESC + '[23~'],
35
+ ['f12', 'F12', ESC + '[24~']
36
+ ].map(([id, label, data]) => ({ id, label, data }))
37
+
38
+ const navButtons = [
39
+ { id: 'esc', label: 'Esc', data: ESC },
40
+ { id: 'tab', label: 'Tab', data: '\t' },
41
+ { id: 'enter', label: 'Enter', data: '\r' },
42
+ { id: 'space', label: 'Space', data: ' ' },
43
+ { id: 'backspace', label: 'Bksp', data: DEL },
44
+ { id: 'arrow-up', label: '↑', data: ESC + '[A' },
45
+ { id: 'arrow-down', label: '↓', data: ESC + '[B' },
46
+ { id: 'arrow-right', label: '→', data: ESC + '[C' },
47
+ { id: 'arrow-left', label: '←', data: ESC + '[D' },
48
+ { id: 'home', label: 'Home', data: ESC + '[H' },
49
+ { id: 'end', label: 'End', data: ESC + '[F' },
50
+ { id: 'pageup', label: 'PgUp', data: ESC + '[5~' },
51
+ { id: 'pagedown', label: 'PgDn', data: ESC + '[6~' },
52
+ { id: 'insert', label: 'Ins', data: ESC + '[2~' },
53
+ { id: 'delete', label: 'Del', data: ESC + '[3~' }
54
+ ]
55
+
56
+ const punctChars = ':;/~|\\`\'".,(){}[]<>=+-_*&^%$#@!?'
57
+ const punctButtons = punctChars.split('').map(ch => ({
58
+ id: 'char-' + ch.charCodeAt(0),
59
+ label: ch,
60
+ data: ch
61
+ }))
62
+
63
+ // full library, de-duplicated by id
64
+ const candidates = []
65
+ const seenIds = new Set()
66
+ ;[...navButtons, ...ctrlButtons, ...fKeys, ...punctButtons].forEach(b => {
67
+ if (!seenIds.has(b.id)) {
68
+ seenIds.add(b.id)
69
+ candidates.push(b)
70
+ }
71
+ })
72
+
73
+ // ids selected by default — mirrors the keys requested in the feature spec
74
+ const DEFAULT_ACTIVE_IDS = [
75
+ 'esc',
76
+ 'tab',
77
+ 'ctrl+c',
78
+ 'ctrl+v',
79
+ 'ctrl+z',
80
+ 'ctrl+y',
81
+ 'ctrl+a',
82
+ 'ctrl+x',
83
+ 'ctrl+s',
84
+ 'ctrl+f',
85
+ 'ctrl+r',
86
+ 'ctrl+g',
87
+ 'ctrl+h',
88
+ 'ctrl+n',
89
+ 'ctrl+p',
90
+ 'ctrl+l',
91
+ 'enter',
92
+ 'char-58',
93
+ 'arrow-up',
94
+ 'arrow-down',
95
+ 'arrow-left',
96
+ 'arrow-right',
97
+ 'f1',
98
+ 'f2',
99
+ 'f3',
100
+ 'f4',
101
+ 'f5',
102
+ 'f6',
103
+ 'f7',
104
+ 'f8',
105
+ 'f9',
106
+ 'f10',
107
+ 'f11',
108
+ 'f12'
109
+ ]
110
+
111
+ export function defaultActive () {
112
+ const byId = new Map(candidates.map(c => [c.id, c]))
113
+ return DEFAULT_ACTIVE_IDS
114
+ .map(id => byId.get(id))
115
+ .filter(Boolean)
116
+ .map(b => ({ ...b }))
117
+ }
118
+
119
+ export function loadActive () {
120
+ const saved = ls.getItemJSON(shortcutBarLsKey, null)
121
+ if (Array.isArray(saved) && saved.length) {
122
+ return saved
123
+ }
124
+ return defaultActive()
125
+ }
126
+
127
+ export function saveActive (arr) {
128
+ ls.setItemJSON(shortcutBarLsKey, arr)
129
+ }
130
+
131
+ export { candidates }
@@ -0,0 +1,205 @@
1
+ /**
2
+ * edit modal for the shortcut bar.
3
+ *
4
+ * - tap a candidate to append it to the active list (order = click order)
5
+ * - tap the × on an active item to remove it
6
+ * - build a custom button (label + literal text to send)
7
+ * - search filters the candidate library
8
+ *
9
+ * Rendered through a portal to document.body so it isn't trapped inside the
10
+ * bar's (z-index 400, 44px tall) stacking context — otherwise higher-z app
11
+ * layers would paint over it on mobile.
12
+ */
13
+
14
+ import { useState } from 'react'
15
+ import { createPortal } from 'react-dom'
16
+ import { Input } from 'antd'
17
+ import {
18
+ CloseOutlined,
19
+ PlusOutlined,
20
+ ReloadOutlined,
21
+ SearchOutlined
22
+ } from '@ant-design/icons'
23
+ import classnames from 'classnames'
24
+ import Modal from '../common/modal'
25
+ import { defaultActive } from './shortcut-bar-defs'
26
+
27
+ const e = window.translate
28
+
29
+ export default function ShortcutBarEdit (props) {
30
+ const {
31
+ active,
32
+ candidates: allCandidates,
33
+ isMobile,
34
+ onCancel,
35
+ onSave
36
+ } = props
37
+
38
+ const [draft, setDraft] = useState(() => active.map(b => ({ ...b })))
39
+ const [keyword, setKeyword] = useState('')
40
+ const [customLabel, setCustomLabel] = useState('')
41
+ const [customData, setCustomData] = useState('')
42
+
43
+ const activeIds = new Set(draft.map(b => b.id))
44
+
45
+ function handleAddCandidate (btn) {
46
+ if (activeIds.has(btn.id)) {
47
+ return
48
+ }
49
+ setDraft(prev => [...prev, { ...btn }])
50
+ }
51
+
52
+ function handleRemoveActive (id) {
53
+ setDraft(prev => prev.filter(b => b.id !== id))
54
+ }
55
+
56
+ function handleAddCustom () {
57
+ const label = customLabel.trim()
58
+ const data = customData
59
+ if (!label) {
60
+ return
61
+ }
62
+ const id = 'custom-' + label + '-' + Date.now()
63
+ setDraft(prev => [...prev, { id, label, data, custom: true }])
64
+ setCustomLabel('')
65
+ setCustomData('')
66
+ }
67
+
68
+ function handleReset () {
69
+ setDraft(defaultActive())
70
+ }
71
+
72
+ function handleSave () {
73
+ onSave(draft)
74
+ }
75
+
76
+ const kw = keyword.trim().toLowerCase()
77
+ const filtered = allCandidates.filter(b => {
78
+ if (!kw) {
79
+ return true
80
+ }
81
+ return b.label.toLowerCase().includes(kw) || b.id.includes(kw)
82
+ })
83
+
84
+ function renderActive (b) {
85
+ return (
86
+ <div
87
+ key={b.id}
88
+ className='shortcut-active-item'
89
+ >
90
+ <span className='shortcut-active-label'>{b.label}</span>
91
+ <button
92
+ type='button'
93
+ className='shortcut-active-remove'
94
+ onClick={() => handleRemoveActive(b.id)}
95
+ >
96
+ <CloseOutlined />
97
+ </button>
98
+ </div>
99
+ )
100
+ }
101
+
102
+ function renderCandidate (b) {
103
+ const added = activeIds.has(b.id)
104
+ return (
105
+ <button
106
+ type='button'
107
+ key={b.id}
108
+ className={classnames('shortcut-candidate-item', { added })}
109
+ onClick={() => handleAddCandidate(b)}
110
+ >
111
+ {b.label}
112
+ </button>
113
+ )
114
+ }
115
+
116
+ const footer = (
117
+ <div className='custom-modal-footer-buttons shortcut-edit-footer'>
118
+ <button
119
+ type='button'
120
+ className='custom-modal-cancel-btn'
121
+ onClick={handleReset}
122
+ >
123
+ <ReloadOutlined /> {e('reset')}
124
+ </button>
125
+ <button
126
+ type='button'
127
+ className='custom-modal-cancel-btn'
128
+ onClick={onCancel}
129
+ >
130
+ {e('cancel')}
131
+ </button>
132
+ <button
133
+ type='button'
134
+ className='custom-modal-ok-btn'
135
+ onClick={handleSave}
136
+ >
137
+ {e('save')}
138
+ </button>
139
+ </div>
140
+ )
141
+ const sst = e('keyboardShortcuts')
142
+ return createPortal(
143
+ <Modal
144
+ open
145
+ title={e('edit')}
146
+ onCancel={onCancel}
147
+ footer={footer}
148
+ width={560}
149
+ zIndex={1100}
150
+ wrapClassName={classnames('shortcut-edit-wrap', { 'is-mobile': isMobile })}
151
+ >
152
+ <div className='shortcut-edit-section'>
153
+ <div className='shortcut-edit-section-title'>{sst}</div>
154
+ <div className='shortcut-active-list'>
155
+ {draft.length === 0 && (
156
+ <div className='shortcut-active-empty'>0 {sst}</div>
157
+ )}
158
+ {draft.map(renderActive)}
159
+ </div>
160
+ </div>
161
+
162
+ <div className='shortcut-edit-section'>
163
+ <div className='shortcut-custom-form'>
164
+ <Input
165
+ prefix={<SearchOutlined />}
166
+ value={keyword}
167
+ onChange={ev => setKeyword(ev.target.value)}
168
+ placeholder={e('search')}
169
+ className='shortcut-search-input'
170
+ allowClear
171
+ />
172
+ </div>
173
+ <div className='shortcut-candidate-list'>
174
+ {filtered.map(renderCandidate)}
175
+ </div>
176
+ </div>
177
+
178
+ <div className='shortcut-edit-section'>
179
+ <div className='shortcut-edit-section-title'>+ {sst}</div>
180
+ <div className='shortcut-custom-form'>
181
+ <Input
182
+ value={customLabel}
183
+ onChange={ev => setCustomLabel(ev.target.value)}
184
+ placeholder={e('label')}
185
+ className='shortcut-custom-input'
186
+ />
187
+ <Input
188
+ value={customData}
189
+ onChange={ev => setCustomData(ev.target.value)}
190
+ placeholder={e('edit')}
191
+ className='shortcut-custom-input'
192
+ />
193
+ <button
194
+ type='button'
195
+ className='custom-modal-ok-btn shortcut-custom-add'
196
+ onClick={handleAddCustom}
197
+ >
198
+ <PlusOutlined /> {e('add')}
199
+ </button>
200
+ </div>
201
+ </div>
202
+ </Modal>,
203
+ document.body
204
+ )
205
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * lazy entry for shortcut-bar.
3
+ *
4
+ * The bar only makes sense on touch devices (it exists to compensate for the
5
+ * lack of a physical keyboard when the system input panel is shown). On
6
+ * non-touch devices we render nothing AND never import the real component, so
7
+ * its code (and the antd/icon deps it pulls in) stays out of the main bundle.
8
+ */
9
+
10
+ import { lazy, Suspense } from 'react'
11
+
12
+ const ShortcutBar = lazy(() => import('./shortcut-bar'))
13
+
14
+ export function isTouchDevice () {
15
+ if (typeof window === 'undefined' || typeof navigator === 'undefined') {
16
+ return false
17
+ }
18
+ const coarse = typeof window.matchMedia === 'function' &&
19
+ window.matchMedia('(pointer: coarse)').matches
20
+ const hasTouch = (navigator.maxTouchPoints || 0) > 0 ||
21
+ ('ontouchstart' in window)
22
+ return Boolean(coarse || hasTouch)
23
+ }
24
+
25
+ export default function ShortcutBarEntry (props) {
26
+ if (!isTouchDevice()) {
27
+ return null
28
+ }
29
+ return (
30
+ <Suspense fallback={null}>
31
+ <ShortcutBar {...props} />
32
+ </Suspense>
33
+ )
34
+ }