@electerm/electerm-react 5.5.9 → 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 (62) 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/download.jsx +12 -0
  5. package/client/common/parse-vv.js +313 -0
  6. package/client/common/resolve.js +20 -14
  7. package/client/common/theme-defaults.js +5 -2
  8. package/client/common/update-check.js +5 -4
  9. package/client/components/ai/ai-history-item.jsx +1 -1
  10. package/client/components/ai/ai-history.jsx +1 -0
  11. package/client/components/ai/ai-history.styl +23 -0
  12. package/client/components/ai/ai-presets.js +13 -2
  13. package/client/components/bg/css-overwrite.jsx +16 -1
  14. package/client/components/bookmark-form/common/color-picker.styl +3 -0
  15. package/client/components/bookmark-form/common/fields.jsx +3 -0
  16. package/client/components/bookmark-form/common/vv-file-field.jsx +137 -0
  17. package/client/components/bookmark-form/config/spice.js +1 -0
  18. package/client/components/common/remote-pointer.js +49 -0
  19. package/client/components/common/remote-session-shell.jsx +137 -0
  20. package/client/components/common/remote-session-shell.styl +80 -0
  21. package/client/components/common/responsive-tabs.jsx +89 -0
  22. package/client/components/common/responsive-tabs.styl +34 -0
  23. package/client/components/footer/footer-entry.jsx +39 -14
  24. package/client/components/icons/ai-icon.styl +4 -1
  25. package/client/components/layout/layout.jsx +7 -1
  26. package/client/components/main/main.jsx +9 -2
  27. package/client/components/main/term-fullscreen.styl +7 -0
  28. package/client/components/quick-commands/quick-command-ai-editor.jsx +255 -0
  29. package/client/components/quick-commands/quick-command-ai-editor.styl +13 -0
  30. package/client/components/quick-commands/quick-commands-box.jsx +23 -5
  31. package/client/components/quick-commands/quick-commands-form-elem.jsx +129 -65
  32. package/client/components/rdp/rdp-session.jsx +94 -124
  33. package/client/components/rdp/rdp.styl +13 -36
  34. package/client/components/session/session.styl +0 -16
  35. package/client/components/setting-panel/list.jsx +2 -2
  36. package/client/components/setting-panel/setting-modal.jsx +8 -3
  37. package/client/components/sftp/sftp-entry.jsx +26 -0
  38. package/client/components/side-panel-r/right-side-panel.styl +11 -1
  39. package/client/components/side-panel-r/side-panel-r.jsx +45 -9
  40. package/client/components/sidebar/bookmark-select.jsx +3 -1
  41. package/client/components/sidebar/index.jsx +13 -4
  42. package/client/components/sidebar/side-panel.jsx +8 -3
  43. package/client/components/sidebar/sidebar-panel.jsx +20 -7
  44. package/client/components/spice/spice-session.jsx +69 -72
  45. package/client/components/spice/spice.styl +10 -19
  46. package/client/components/terminal/shortcut-bar.styl +14 -0
  47. package/client/components/terminal/unix-timestamp-tooltip.jsx +27 -1
  48. package/client/components/terminal-info/terminal-info.styl +3 -0
  49. package/client/components/theme/terminal-theme-list.styl +3 -0
  50. package/client/components/theme/theme-ai-editor.jsx +2 -2
  51. package/client/components/theme/theme-form.jsx +20 -23
  52. package/client/components/theme/theme-form.styl +8 -10
  53. package/client/components/theme/theme-list-item.jsx +59 -25
  54. package/client/components/vnc/vnc-session.jsx +72 -104
  55. package/client/components/vnc/vnc.styl +25 -16
  56. package/client/css/mobile.styl +28 -0
  57. package/client/store/common.js +44 -0
  58. package/client/store/init-state.js +2 -1
  59. package/client/store/setting.js +4 -0
  60. package/client/store/sidebar.js +5 -1
  61. package/client/store/store.js +9 -2
  62. package/package.json +1 -1
@@ -14,7 +14,6 @@ import {
14
14
  DownloadOutlined
15
15
  } from '@ant-design/icons'
16
16
  import {
17
- Spin,
18
17
  Select,
19
18
  Tooltip
20
19
  } from 'antd'
@@ -23,6 +22,8 @@ import scanCode from './code-scan'
23
22
  import resolutions from './resolutions'
24
23
  import { readClipboardAsync } from '../../common/clipboard'
25
24
  import RemoteFloatControl from '../common/remote-float-control'
25
+ import RemoteSessionShell from '../common/remote-session-shell'
26
+ import { eventToRemotePos } from '../common/remote-pointer'
26
27
  import SwitchLabel from '../common/switch'
27
28
  import HelpIcon from '../common/help-icon'
28
29
  import Modal from '../common/modal'
@@ -102,6 +103,12 @@ export default class RdpSession extends PureComponent {
102
103
 
103
104
  runInitScript = () => { }
104
105
 
106
+ // fullscreen always fills the screen, the per tab setting only applies
107
+ // windowed; the switch is not reachable in fullscreen anyway
108
+ getFit = () => {
109
+ return !!this.props.fullscreen || !!this.state.scaleViewport
110
+ }
111
+
105
112
  setStatus = status => {
106
113
  const id = this.props.tab?.id
107
114
  this.props.editTab(id, {
@@ -458,30 +465,14 @@ export default class RdpSession extends PureComponent {
458
465
  canvas.addEventListener('mousemove', (e) => {
459
466
  if (!this.session) return
460
467
  try {
461
- const rect = canvas.getBoundingClientRect()
462
- const { scaleViewport } = this.state
463
- let scaleX = canvas.width / rect.width
464
- let scaleY = canvas.height / rect.height
465
- let offsetX = 0
466
- let offsetY = 0
467
- if (scaleViewport) {
468
- const containerRatio = rect.width / rect.height
469
- const canvasRatio = canvas.width / canvas.height
470
- let renderWidth, renderHeight
471
- if (containerRatio > canvasRatio) {
472
- renderHeight = rect.height
473
- renderWidth = rect.height * canvasRatio
474
- offsetX = (rect.width - renderWidth) / 2
475
- } else {
476
- renderWidth = rect.width
477
- renderHeight = rect.width / canvasRatio
478
- offsetY = (rect.height - renderHeight) / 2
479
- }
480
- scaleX = canvas.width / renderWidth
481
- scaleY = canvas.height / renderHeight
482
- }
483
- const x = Math.round((e.clientX - rect.left - offsetX) * scaleX)
484
- const y = Math.round((e.clientY - rect.top - offsetY) * scaleY)
468
+ // the canvas is rendered with object-fit: contain, so the pointer has
469
+ // to be mapped through the real surface rect (see remote-pointer.js)
470
+ const { x, y } = eventToRemotePos(
471
+ e,
472
+ canvas,
473
+ canvas.width,
474
+ canvas.height
475
+ )
485
476
  const event = window.ironRdp.DeviceEvent.mouseMove(x, y)
486
477
  const tx = new window.ironRdp.InputTransaction()
487
478
  tx.addEvent(event)
@@ -705,12 +696,7 @@ export default class RdpSession extends PureComponent {
705
696
  this.setupInputHandlers()
706
697
  }
707
698
 
708
- renderControl = () => {
709
- const contrlProps = this.getControlProps({
710
- fixedPosition: false,
711
- showExitFullscreen: false,
712
- className: 'mg1l'
713
- })
699
+ renderControlLeft = () => {
714
700
  const {
715
701
  id,
716
702
  hasRemoteFiles,
@@ -730,61 +716,67 @@ export default class RdpSession extends PureComponent {
730
716
  const uploadTitle = window.translate('upload') || 'Upload files to remote'
731
717
  const downloadTitle = window.translate('download') || 'Download files from remote'
732
718
  return (
733
- <div
734
- className='pd1 fix session-v-info block'
735
- >
736
- <div className='fleft'>
737
- <ReloadOutlined
738
- onClick={this.handleReInit}
739
- className='mg2r mg1l pointer'
740
- />
741
- <Select
742
- {...sleProps}
743
- >
744
- {
745
- this.getAllRes().map(d => {
746
- const v = d.id
747
- return (
748
- <Option
749
- key={v}
750
- value={v}
751
- >
752
- {d.width}x{d.height}
753
- </Option>
754
- )
755
- })
756
- }
757
- </Select>
758
- <EditOutlined
759
- onClick={this.handleEditResolutions}
760
- className='mg2r mg1l pointer'
761
- />
762
- {this.renderInfo()}
763
- <SwitchLabel
764
- {...scaleProps}
719
+ <>
720
+ <ReloadOutlined
721
+ onClick={this.handleReInit}
722
+ className='mg2r mg1l pointer'
723
+ />
724
+ <Select
725
+ {...sleProps}
726
+ >
727
+ {
728
+ this.getAllRes().map(d => {
729
+ const v = d.id
730
+ return (
731
+ <Option
732
+ key={v}
733
+ value={v}
734
+ >
735
+ {d.width}x{d.height}
736
+ </Option>
737
+ )
738
+ })
739
+ }
740
+ </Select>
741
+ <EditOutlined
742
+ onClick={this.handleEditResolutions}
743
+ className='mg2r mg1l pointer'
744
+ />
745
+ {this.renderInfo()}
746
+ <SwitchLabel
747
+ {...scaleProps}
748
+ />
749
+ <Tooltip title={uploadTitle}>
750
+ <UploadOutlined
751
+ onClick={this.handleUploadButtonClick}
752
+ className={`mg1r mg2l pointer rdp-file-transfer-btn${uploadReady ? ' rdp-download-flash' : ''}`}
765
753
  />
766
- <Tooltip title={uploadTitle}>
767
- <UploadOutlined
768
- onClick={this.handleUploadButtonClick}
769
- className={`mg1r mg2l pointer rdp-file-transfer-btn${uploadReady ? ' rdp-download-flash' : ''}`}
770
- />
771
- </Tooltip>
772
- <Tooltip title={downloadTitle}>
773
- <DownloadOutlined
774
- onClick={this.handleDownloadButtonClick}
775
- className={`mg2r mg1l pointer rdp-file-transfer-btn${hasRemoteFiles ? ' rdp-download-flash' : ' rdp-download-disabled'}`}
776
- />
777
- </Tooltip>
778
- <HelpIcon
779
- link='https://github.com/electerm/electerm/wiki/RDP-File-Transfer'
780
- className='mg2r mg1l'
754
+ </Tooltip>
755
+ <Tooltip title={downloadTitle}>
756
+ <DownloadOutlined
757
+ onClick={this.handleDownloadButtonClick}
758
+ className={`mg2r mg1l pointer rdp-file-transfer-btn${hasRemoteFiles ? ' rdp-download-flash' : ' rdp-download-disabled'}`}
781
759
  />
782
- </div>
783
- <div className='fright'>
784
- {this.props.fullscreenIcon()}
785
- <RemoteFloatControl {...contrlProps} />
786
- </div>
787
- </div>
760
+ </Tooltip>
761
+ <HelpIcon
762
+ link='https://github.com/electerm/electerm/wiki/RDP-File-Transfer'
763
+ className='mg2r mg1l'
764
+ />
765
+ </>
766
+ )
767
+ }
768
+
769
+ renderControlRight = () => {
770
+ const contrlProps = this.getControlProps({
771
+ fixedPosition: false,
772
+ showExitFullscreen: false,
773
+ className: 'mg1l'
774
+ })
775
+ return (
776
+ <>
777
+ {this.props.fullscreenIcon()}
778
+ <RemoteFloatControl {...contrlProps} />
779
+ </>
788
780
  )
789
781
  }
790
782
 
@@ -802,48 +794,26 @@ export default class RdpSession extends PureComponent {
802
794
  }
803
795
 
804
796
  render () {
805
- const { width: w, height: h } = this.props
806
- const { width, height, loading, scaleViewport } = this.state
807
- const innerWidth = w - 10
808
- const innerHeight = h - 80
809
- const wrapperStyle = {
810
- width: innerWidth + 'px',
811
- height: innerHeight + 'px',
812
- overflow: scaleViewport ? 'hidden' : 'auto'
813
- }
814
- const canvasProps = {
815
- width,
816
- height,
817
- tabIndex: 0
818
- }
819
- const cls = `rdp-session-wrap session-v-wrap${scaleViewport ? ' scale-viewport' : ''}`
820
- const sessProps = {
821
- className: cls,
822
- style: {
823
- width: w + 'px',
824
- height: h + 'px'
825
- }
826
- }
797
+ const { width, height, loading } = this.state
827
798
  const controlProps = this.getControlProps()
828
799
  return (
829
- <Spin spinning={loading}>
830
- <div
831
- {...sessProps}
832
- >
833
- {this.renderControl()}
834
- <RemoteFloatControl {...controlProps} />
835
- <div
836
- style={wrapperStyle}
837
- className='rdp-scroll-wrapper s-scroll-wrapper'
838
- >
839
- <canvas
840
- {...canvasProps}
841
- ref={this.canvasRef}
842
- />
843
- </div>
844
- {this.renderCredPrompt()}
845
- </div>
846
- </Spin>
800
+ <RemoteSessionShell
801
+ loading={loading}
802
+ fit={this.getFit()}
803
+ wrapClassName='rdp-session-wrap'
804
+ controlLeft={this.renderControlLeft()}
805
+ controlRight={this.renderControlRight()}
806
+ floatControl={<RemoteFloatControl {...controlProps} />}
807
+ overlay={this.renderCredPrompt()}
808
+ >
809
+ <canvas
810
+ className='rdp-canvas'
811
+ width={width}
812
+ height={height}
813
+ tabIndex={0}
814
+ ref={this.canvasRef}
815
+ />
816
+ </RemoteSessionShell>
847
817
  )
848
818
  }
849
819
  }
@@ -1,36 +1,18 @@
1
+ // RDP only styling — the session layout (control bar, viewport, fit mode)
2
+ // lives in the shared remote session shell: common/remote-session-shell.styl
3
+
4
+ // fit: the canvas box fills the viewport and the remote surface is letterboxed
5
+ // inside it with object-fit: contain, so the aspect ratio is kept and the
6
+ // letterbox keeps the app background
1
7
  .rdp-session-wrap
2
- display: flex
3
- flex-direction: column
4
- justify-content: center
5
- align-items: center
6
- .session-v-info
7
- position: absolute
8
- top: 0
9
- left: 0
10
- width: 100%
11
- &.scale-viewport
12
- .rdp-scroll-wrapper canvas
13
- width: 100% !important
14
- height: 100% !important
15
- object-fit: contain
8
+ &.fit
9
+ .rdp-canvas
10
+ object-fit contain
16
11
 
17
- .s-scroll-wrapper
18
- &::-webkit-scrollbar
19
- width 16px
20
- height 16px
21
- background var(--main-darker)
22
- &::-webkit-scrollbar-track
23
- background var(--main-darker)
24
- box-shadow inset 0 0 5px var(--main-darker)
25
- &::-webkit-scrollbar-thumb
26
- background var(--primary)
27
- border-radius 0
28
- &::-webkit-scrollbar-corner
29
- background var(--main-darker)
30
- .rdp-scroll-wrapper
31
- position relative
32
- background var(--main)
33
- z-index 299
12
+ // fit off: rendered at the remote desktop size, the viewport scrolls
13
+ .rdp-canvas
14
+ display block
15
+ outline none
34
16
 
35
17
  .rdp-file-transfer-btn
36
18
  font-size: 16px
@@ -45,9 +27,6 @@
45
27
  &:hover
46
28
  color: var(--text)
47
29
 
48
- canvas
49
- outline: none
50
-
51
30
  @keyframes rdp-download-flash
52
31
  0%, 100%
53
32
  color: var(--primary)
@@ -58,5 +37,3 @@ canvas
58
37
 
59
38
  .rdp-download-flash
60
39
  animation: rdp-download-flash 1s ease-in-out infinite
61
-
62
-
@@ -71,22 +71,6 @@
71
71
  overflow hidden
72
72
  .session-v-info
73
73
  line-height 30px
74
- .vnc-scroll-wrapper
75
- position relative
76
- background var(--main)
77
- z-index 299
78
- &::-webkit-scrollbar
79
- width 16px
80
- height 16px
81
- background var(--main-darker)
82
- &::-webkit-scrollbar-track
83
- background var(--main-darker)
84
- box-shadow inset 0 0 5px var(--main-darker)
85
- &::-webkit-scrollbar-thumb
86
- background var(--primary)
87
- border-radius 0
88
- &::-webkit-scrollbar-corner
89
- background var(--main-darker)
90
74
 
91
75
  .not-split-view > .ant-splitter-bar .ant-splitter-bar-dragger
92
76
  display none
@@ -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
@@ -4,10 +4,11 @@
4
4
 
5
5
  import { auto } from 'manate/react'
6
6
  import { pick } from 'lodash-es'
7
- import { Tabs, Spin } from 'antd'
7
+ import { Spin } from 'antd'
8
8
  import { lazy, Suspense } from 'react'
9
9
  import SettingModal from './setting-wrap'
10
10
  import LazyBoundary from '../common/lazy-boundary'
11
+ import ResponsiveTabs from '../common/responsive-tabs'
11
12
  import {
12
13
  settingMap,
13
14
  modals,
@@ -132,11 +133,15 @@ export default auto(function SettingModalWrap (props) {
132
133
  onChange: store.handleChangeSettingTab,
133
134
  destroyOnHidden: true,
134
135
  className: 'setting-tabs',
135
- type: 'card'
136
+ type: 'card',
137
+ // the panel turns into a phone style drill-down at 800px (same
138
+ // breakpoint as the media query in setting-wrap.styl), collapse the tab
139
+ // bar there too instead of letting antd hide the overflow tabs
140
+ isMobile: store.innerWidth <= settingPanelMobileBreakpoint
136
141
  }
137
142
  return (
138
143
  <>
139
- <Tabs
144
+ <ResponsiveTabs
140
145
  {...tabsProps}
141
146
  />
142
147
  <LazyBoundary>
@@ -293,11 +293,29 @@ export default class Sftp extends Component {
293
293
  if (!path && this.sftp) {
294
294
  path = await this.getPwd(this.props.tab.username)
295
295
  }
296
+ if (!path) {
297
+ // No sftp connection yet (still connecting, or the previous attempt
298
+ // failed). There is no remote home to resolve, and navigating to an
299
+ // unknown path would blank the panel, so (re)start the connection and
300
+ // let its own listing decide the path.
301
+ if (
302
+ !this.sftp &&
303
+ !this.state.loadingSftp &&
304
+ !this.state.remoteLoading
305
+ ) {
306
+ this.initData(this.terminalId, this.port)
307
+ }
308
+ return
309
+ }
296
310
  path = normalizeRemotePath(path)
297
311
  } else {
298
312
  path = this.getLocalHome()
299
313
  }
300
314
 
315
+ if (typeof path !== 'string' || !path) {
316
+ return
317
+ }
318
+
301
319
  this.setState({
302
320
  [n]: path,
303
321
  [nt]: path
@@ -1033,6 +1051,9 @@ export default class Sftp extends Component {
1033
1051
  goParent = (type) => {
1034
1052
  const n = `${type}Path`
1035
1053
  const p = this.state[n]
1054
+ if (typeof p !== 'string' || !p) {
1055
+ return
1056
+ }
1036
1057
  let np = resolve(p, '..')
1037
1058
  if (type === typeMap.remote) {
1038
1059
  np = normalizeRemotePath(np)
@@ -1136,6 +1157,11 @@ export default class Sftp extends Component {
1136
1157
 
1137
1158
  renderParentItem = (type) => {
1138
1159
  const currentPath = this.state[`${type}Path`]
1160
+ // The path can be momentarily unknown (e.g. the remote home while the sftp
1161
+ // client is still connecting). Show no parent item rather than throwing.
1162
+ if (typeof currentPath !== 'string') {
1163
+ return null
1164
+ }
1139
1165
  const parentPath = resolve(currentPath, '..')
1140
1166
  // Don't render parent item if we're at the root
1141
1167
  if (parentPath === currentPath) {
@@ -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
  )