@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
@@ -2,13 +2,13 @@
2
2
  * theme list render
3
3
  */
4
4
 
5
- import { useState } from 'react'
5
+ import { useEffect, useState } from 'react'
6
6
  import {
7
7
  CheckCircleOutlined,
8
+ CloseOutlined,
8
9
  PlusOutlined,
9
10
  SunOutlined,
10
- MoonOutlined,
11
- EyeOutlined
11
+ MoonOutlined
12
12
  } from '@ant-design/icons'
13
13
  import { Tag, Tooltip, Button, Space } from 'antd'
14
14
  import classnames from 'classnames'
@@ -30,46 +30,72 @@ export default function ThemeListItem (props) {
30
30
  const [tooltipVisible, setTooltipVisible] = useState(false)
31
31
  const [isPreviewing, setIsPreviewing] = useState(false)
32
32
 
33
- function handleClickApply () {
34
- setTooltipVisible(false)
35
- setIsPreviewing(false)
36
- delete window.originalTheme
33
+ function startPreview () {
34
+ // keep the very first original, hopping between themes must never
35
+ // overwrite it with a theme that is itself only a preview
36
+ if (!window.originalTheme) {
37
+ window.originalTheme = store.config.theme
38
+ }
37
39
  store.setTheme(item.id)
40
+ setIsPreviewing(true)
38
41
  }
39
42
 
40
- function handleClickPreview () {
41
- if (!isPreviewing) {
42
- // Store current theme ID before changing
43
- const currentTheme = window.store.config.theme
44
- window.originalTheme = currentTheme
45
- // Apply the preview theme
46
- store.setTheme(item.id)
47
- setIsPreviewing(true)
43
+ function cancelPreview () {
44
+ if (window.originalTheme) {
45
+ store.setTheme(window.originalTheme)
46
+ delete window.originalTheme
48
47
  }
48
+ setIsPreviewing(false)
49
49
  }
50
50
 
51
- function handleTooltipVisibleChange (visible) {
52
- setTooltipVisible(visible)
53
- if (!visible && isPreviewing) {
54
- // Restore original theme when tooltip closes during preview
51
+ useEffect(() => {
52
+ return () => {
53
+ // leaving the list mid preview (switching settings tab, filtering the
54
+ // list, ...) must not leave the user stuck on a theme never applied
55
55
  if (window.originalTheme) {
56
56
  store.setTheme(window.originalTheme)
57
57
  delete window.originalTheme
58
58
  }
59
- setIsPreviewing(false)
59
+ }
60
+ }, [])
61
+
62
+ function handleClickApply () {
63
+ delete window.originalTheme
64
+ setIsPreviewing(false)
65
+ setTooltipVisible(false)
66
+ store.setTheme(item.id)
67
+ }
68
+
69
+ function handleClickCancel () {
70
+ cancelPreview()
71
+ setTooltipVisible(false)
72
+ }
73
+
74
+ function handleTooltipVisibleChange (visible) {
75
+ setTooltipVisible(visible)
76
+ // opening the popup is the preview itself; every other way of closing it
77
+ // (click elsewhere, esc, clicking the icon again) rolls the preview back
78
+ if (visible) {
79
+ startPreview()
80
+ } else {
81
+ cancelPreview()
60
82
  }
61
83
  }
62
84
 
85
+ function handleClickApplyIcon (e) {
86
+ // previewing must not also load the theme into the edit form
87
+ e.stopPropagation()
88
+ }
89
+
63
90
  function renderTooltipContent () {
64
91
  return (
65
92
  <Space.Compact>
66
93
  <Button
67
94
  size='small'
68
- icon={<EyeOutlined />}
69
- onClick={handleClickPreview}
70
- type={isPreviewing ? 'primary' : 'default'}
95
+ icon={<CloseOutlined />}
96
+ onClick={handleClickCancel}
71
97
  >
72
- {e('preview')}
98
+ {e('cancel')}
73
99
  </Button>
74
100
  <Button
75
101
  size='small'
@@ -96,7 +122,15 @@ export default function ThemeListItem (props) {
96
122
  placement='top'
97
123
  >
98
124
  <CheckCircleOutlined
99
- className='pointer list-item-apply'
125
+ className={
126
+ classnames(
127
+ 'pointer list-item-apply',
128
+ {
129
+ 'list-item-apply-previewing': isPreviewing
130
+ }
131
+ )
132
+ }
133
+ onClick={handleClickApplyIcon}
100
134
  />
101
135
  </Tooltip>
102
136
  )
@@ -11,6 +11,34 @@
11
11
  .remote-monitor-bar
12
12
  position fixed
13
13
 
14
+ // Side panels are full-screen drawers on mobile: they start at the viewport
15
+ // edge (the left one covers the icon bar too, so no strip of terminal is left
16
+ // showing) and run to the bottom edge — footer included. On desktop the
17
+ // footer is reserved *beside* the panel because it holds the panel's own
18
+ // trigger; on mobile the drawer is dismissed from its header (✕ / the
19
+ // bookmark/history tabs) instead, so there is nothing to keep clear. The
20
+ // inline width already comes from store.leftSidePanelWidth / rightPanelWidth,
21
+ // which are the viewport width here.
22
+ // Both are on the fixed frame (the right panel explicitly: its bottom edge
23
+ // must not drift from the fixed footer when the visual viewport changes), and
24
+ // both have to outrank the footer's 200 — it comes later in the DOM, so at an
25
+ // equal rank it would paint over the drawer. The left drawer lives inside
26
+ // .sidebar, which is its own stacking context (z-index 100), so lifting the
27
+ // drawer alone is not enough: the whole sidebar column has to clear the
28
+ // footer. The right panel is a root-level child, so its own z-index suffices.
29
+ // The icon bar takes no extra room from this: it is 43px wide and the footer
30
+ // starts at 44px.
31
+ .sidebar
32
+ z-index 250
33
+ .sidebar-list
34
+ left 0
35
+ bottom 0
36
+ z-index 250
37
+ .right-side-panel
38
+ position fixed
39
+ bottom 0
40
+ z-index 250
41
+
14
42
  // ---- 1. Tabs: show only current tab + dropdown, close icon always visible
15
43
  .tabs.mobile-tabs
16
44
  display flex
@@ -10,6 +10,7 @@ import {
10
10
  leftSidePanelWidthKey,
11
11
  leftSideBarOpenKey,
12
12
  rightSidebarWidthKey,
13
+ rightPanelPinnedKey,
13
14
  addPanelWidthLsKey,
14
15
  connectionMap,
15
16
  lastAiChatSessionIdKey,
@@ -42,6 +43,18 @@ export default Store => {
42
43
  window.store.setConfig(ext)
43
44
  }
44
45
 
46
+ // The footer info icon is a toggle: the same click opens and closes the
47
+ // panel, so the trigger never has to be hunted down in the panel header.
48
+ Store.prototype.toggleInfoPanel = action(function () {
49
+ const { store } = window
50
+ const isOpen = store.rightPanelVisible && store.rightPanelTab === 'info'
51
+ store.rightPanelVisible = !isOpen
52
+ store.rightPanelTab = 'info'
53
+ if (!isOpen) {
54
+ store.openInfoPanelAction()
55
+ }
56
+ })
57
+
45
58
  Store.prototype.openInfoPanel = action(function () {
46
59
  const { store } = window
47
60
  store.rightPanelVisible = true
@@ -122,7 +135,13 @@ export default Store => {
122
135
  Object.assign(window.store._termSearchOptions, update)
123
136
  }
124
137
 
138
+ // Both panel widths are desktop-only preferences: on mobile the panel is a
139
+ // full-width drawer whose width is fixed, so a drag (or any other caller)
140
+ // must not overwrite the desktop value that is still in localStorage.
125
141
  Store.prototype.setLeftSidePanelWidth = function (v) {
142
+ if (window.store.isMobile) {
143
+ return
144
+ }
126
145
  ls.setItem(leftSidePanelWidthKey, v)
127
146
  window.store._leftSidePanelWidth = v
128
147
  }
@@ -144,9 +163,22 @@ export default Store => {
144
163
  }
145
164
 
146
165
  Store.prototype.setRightSidePanelWidth = function (v) {
166
+ if (window.store.isMobile) {
167
+ return
168
+ }
147
169
  ls.setItem(rightSidebarWidthKey, v)
148
170
  window.store._rightPanelWidth = v
149
171
  }
172
+
173
+ // Persist the pin the same way the left sidebar does (sidebarPinnedKey):
174
+ // pinned is a durable layout preference, not a per-session toggle. It matters
175
+ // more here because pinned and unpinned are visibly different modes — the
176
+ // unpinned panel is an overlay that clears the footer, the pinned one is a
177
+ // full-height dock.
178
+ Store.prototype.setRightPanelPinned = function (v) {
179
+ ls.setItem(rightPanelPinnedKey, v + '')
180
+ window.store.rightPanelPinned = v
181
+ }
150
182
  Store.prototype.beforeExit = function (evt) {
151
183
  const { confirmBeforeExit } = window.store.config
152
184
  if (
@@ -286,6 +318,18 @@ export default Store => {
286
318
  }
287
319
  }
288
320
 
321
+ // Same toggle contract as toggleInfoPanel: the footer AI button opens and
322
+ // closes its own panel. The open path must go through handleOpenAIPanel so
323
+ // the missing-config prompt still fires; closing must not touch config.
324
+ Store.prototype.toggleAIPanel = function () {
325
+ const { store } = window
326
+ if (store.rightPanelVisible && store.rightPanelTab === 'ai') {
327
+ store.rightPanelVisible = false
328
+ return
329
+ }
330
+ store.handleOpenAIPanel()
331
+ }
332
+
289
333
  Store.prototype.explainWithAi = function (txt) {
290
334
  const { store } = window
291
335
  store.handleOpenAIPanel()
@@ -18,6 +18,7 @@ import {
18
18
  localAddrBookmarkLsKey,
19
19
  leftSidePanelWidthKey,
20
20
  rightSidebarWidthKey,
21
+ rightPanelPinnedKey,
21
22
  addPanelWidthLsKey,
22
23
  qmSortByFrequencyKey,
23
24
  resolutionsLsKey,
@@ -125,7 +126,7 @@ export default () => {
125
126
  terminalInfoProps: {},
126
127
  rightPanelVisible: false,
127
128
  rightPanelTab: 'info',
128
- rightPanelPinned: false,
129
+ rightPanelPinned: ls.getItem(rightPanelPinnedKey) === 'true',
129
130
  _rightPanelWidth: parseInt(ls.getItem(rightSidebarWidthKey), 10) || 500,
130
131
  showAIConfigModal: false,
131
132
 
@@ -25,7 +25,11 @@ export default Store => {
25
25
  // through this one method.
26
26
  Store.prototype.openLeftSidePanel = function (field = 'bookmarks') {
27
27
  const { store } = window
28
- if (store.pinned) {
28
+ // A pinned panel is already open, so there is nothing to toggle — except
29
+ // on mobile, where pinning is not a reachable mode (no pin control) and
30
+ // the panel is a full-width overlay. Honouring a stale desktop pin here
31
+ // would leave the bookmark button dead on arrival.
32
+ if (store.pinned && !store.isMobile) {
29
33
  return
30
34
  }
31
35
  store.setOpenedSideBar(store.openedSideBar ? '' : field)
@@ -61,10 +61,15 @@ class Store {
61
61
  return window.store.innerWidth
62
62
  }
63
63
 
64
+ // Mobile turns a side panel into a full-screen drawer: it spans the whole
65
+ // viewport and its width is not user-adjustable. So the stored desktop width
66
+ // is ignored here, and so is the icon bar (the drawer covers it) — and the
67
+ // old 500px cap, which left a strip of terminal showing on windows between
68
+ // 500 and 600px, i.e. the top of the mobile range.
64
69
  get leftSidePanelWidth () {
65
70
  const { store } = window
66
71
  if (store.isMobile) {
67
- return Math.min(store.innerWidth - store.leftSideBarWidth, 500)
72
+ return store.innerWidth
68
73
  }
69
74
  return store._leftSidePanelWidth
70
75
  }
@@ -78,10 +83,12 @@ class Store {
78
83
  return store._leftSideBarOpen ? sidebarWidth : 0
79
84
  }
80
85
 
86
+ // Same as leftSidePanelWidth: full-screen drawer on mobile, width not
87
+ // user-adjustable.
81
88
  get rightPanelWidth () {
82
89
  const { store } = window
83
90
  if (store.isMobile) {
84
- return Math.min(store.innerWidth, 500)
91
+ return store.innerWidth
85
92
  }
86
93
  return store._rightPanelWidth
87
94
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@electerm/electerm-react",
3
- "version": "5.5.15",
3
+ "version": "5.5.25",
4
4
  "description": "react components src for electerm",
5
5
  "main": "./client/components/main/main.jsx",
6
6
  "license": "MIT",