@linxin666/dsh-ssh 0.3.23 → 0.3.24
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.
- package/lib/client.js +55 -11
- package/lib/client.js.map +1 -1
- package/lib/types/client/sidebar-entry-core.d.ts +32 -0
- package/package.json +2 -1
- package/src/client/sidebar-entry-core.ts +96 -12
|
@@ -1,3 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One trailing action button seated at the entry row's right edge (e.g. a
|
|
3
|
+
* refresh command or a collapse chevron). Rows with actions switch from the
|
|
4
|
+
* classic single-button structure to a container with a main button plus the
|
|
5
|
+
* action buttons (nested interactive elements are invalid HTML); rows without
|
|
6
|
+
* actions keep the classic structure untouched.
|
|
7
|
+
*/
|
|
8
|
+
export interface SidebarEntryAction {
|
|
9
|
+
/** Stable id stamped as data-dsh-entry-action="<id>" (CSS hooks and tests). */
|
|
10
|
+
id: string;
|
|
11
|
+
/** Inline icon markup while the row is active (always, when inactiveIcon is unset). */
|
|
12
|
+
icon: string;
|
|
13
|
+
/**
|
|
14
|
+
* Optional icon while the row is inactive. Setting it makes the action a
|
|
15
|
+
* state mirror (a collapse chevron): the core swaps the icon and stamps
|
|
16
|
+
* aria-expanded on every active-state change. Requires the active bridge.
|
|
17
|
+
*/
|
|
18
|
+
inactiveIcon?: string;
|
|
19
|
+
/** Localized accessible label (aria-label + title); receives the active state. */
|
|
20
|
+
label(active: boolean): string;
|
|
21
|
+
/**
|
|
22
|
+
* Click handler; receives the button so the caller can drive transient
|
|
23
|
+
* affordances (a refresh cooldown) without a re-render.
|
|
24
|
+
*/
|
|
25
|
+
onClick(button: HTMLButtonElement): void;
|
|
26
|
+
}
|
|
1
27
|
/** Per-package configuration for one sidebar entry row. */
|
|
2
28
|
export interface SidebarEntryOptions {
|
|
3
29
|
/** Full attribute name identifying the injected row (idempotency key), e.g. 'data-dsh-ssh-entry'. */
|
|
@@ -31,6 +57,12 @@ export interface SidebarEntryOptions {
|
|
|
31
57
|
};
|
|
32
58
|
/** Click action (open/toggle the owning panel). */
|
|
33
59
|
onToggle(): void;
|
|
60
|
+
/**
|
|
61
|
+
* Optional trailing action buttons at the row's right edge. Requires the
|
|
62
|
+
* package CSS to seat entryMain / entryAction; empty keeps the classic
|
|
63
|
+
* single-button row.
|
|
64
|
+
*/
|
|
65
|
+
actions?: readonly SidebarEntryAction[];
|
|
34
66
|
/** Family-block position: 'before' inserts ahead of sibling plugin rows, 'after' behind them. */
|
|
35
67
|
position: 'before' | 'after';
|
|
36
68
|
/**
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@linxin666/dsh-ssh",
|
|
3
3
|
"description": "Remote SSH operations for the dsh web GUI: host config store (~/.dsh/dsh-ssh.json, import from ~/.ssh/config), a persistent ssh2 connection pool with jump-host support, exec / PTY web terminal / SFTP transfer / local port-forward tunnels / cluster execution, plus agent tools (ssh_list, ssh_exec, ssh_upload, ssh_download, ssh_tunnel, ssh_cluster). Hot-pluggable — mounted via ~/.dsh/cordis.patch.yml + a profile node_modules symlink, no dsh source changes.",
|
|
4
|
-
"version": "0.3.
|
|
4
|
+
"version": "0.3.24",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
7
7
|
"node": "^22.19.0 || >=24.0.0"
|
|
@@ -69,6 +69,7 @@
|
|
|
69
69
|
"@types/react-dom": "^18.3.5",
|
|
70
70
|
"@types/ssh2": "^1.15.5",
|
|
71
71
|
"@types/ws": "^8.5.13",
|
|
72
|
+
"@vitest/coverage-v8": "^3.2.7",
|
|
72
73
|
"jsdom": "^25.0.0",
|
|
73
74
|
"react": "^18.3.1",
|
|
74
75
|
"react-dom": "^18.3.1",
|
|
@@ -16,6 +16,33 @@
|
|
|
16
16
|
*/
|
|
17
17
|
import { subscribeBodyInvalidations } from './body-mutations.ts'
|
|
18
18
|
|
|
19
|
+
/**
|
|
20
|
+
* One trailing action button seated at the entry row's right edge (e.g. a
|
|
21
|
+
* refresh command or a collapse chevron). Rows with actions switch from the
|
|
22
|
+
* classic single-button structure to a container with a main button plus the
|
|
23
|
+
* action buttons (nested interactive elements are invalid HTML); rows without
|
|
24
|
+
* actions keep the classic structure untouched.
|
|
25
|
+
*/
|
|
26
|
+
export interface SidebarEntryAction {
|
|
27
|
+
/** Stable id stamped as data-dsh-entry-action="<id>" (CSS hooks and tests). */
|
|
28
|
+
id: string
|
|
29
|
+
/** Inline icon markup while the row is active (always, when inactiveIcon is unset). */
|
|
30
|
+
icon: string
|
|
31
|
+
/**
|
|
32
|
+
* Optional icon while the row is inactive. Setting it makes the action a
|
|
33
|
+
* state mirror (a collapse chevron): the core swaps the icon and stamps
|
|
34
|
+
* aria-expanded on every active-state change. Requires the active bridge.
|
|
35
|
+
*/
|
|
36
|
+
inactiveIcon?: string
|
|
37
|
+
/** Localized accessible label (aria-label + title); receives the active state. */
|
|
38
|
+
label(active: boolean): string
|
|
39
|
+
/**
|
|
40
|
+
* Click handler; receives the button so the caller can drive transient
|
|
41
|
+
* affordances (a refresh cooldown) without a re-render.
|
|
42
|
+
*/
|
|
43
|
+
onClick(button: HTMLButtonElement): void
|
|
44
|
+
}
|
|
45
|
+
|
|
19
46
|
/** Per-package configuration for one sidebar entry row. */
|
|
20
47
|
export interface SidebarEntryOptions {
|
|
21
48
|
/** Full attribute name identifying the injected row (idempotency key), e.g. 'data-dsh-ssh-entry'. */
|
|
@@ -47,6 +74,12 @@ export interface SidebarEntryOptions {
|
|
|
47
74
|
refresh?: { subscribe(listener: () => void): () => void }
|
|
48
75
|
/** Click action (open/toggle the owning panel). */
|
|
49
76
|
onToggle(): void
|
|
77
|
+
/**
|
|
78
|
+
* Optional trailing action buttons at the row's right edge. Requires the
|
|
79
|
+
* package CSS to seat entryMain / entryAction; empty keeps the classic
|
|
80
|
+
* single-button row.
|
|
81
|
+
*/
|
|
82
|
+
actions?: readonly SidebarEntryAction[]
|
|
50
83
|
/** Family-block position: 'before' inserts ahead of sibling plugin rows, 'after' behind them. */
|
|
51
84
|
position: 'before' | 'after'
|
|
52
85
|
/**
|
|
@@ -84,10 +117,12 @@ function newSessionButton(root: HTMLElement): HTMLButtonElement | undefined {
|
|
|
84
117
|
return undefined
|
|
85
118
|
}
|
|
86
119
|
|
|
87
|
-
/** Build the entry row (
|
|
88
|
-
function createEntry(options: SidebarEntryOptions): { entry:
|
|
89
|
-
const
|
|
90
|
-
|
|
120
|
+
/** Build the entry row (detached; inserted once the shell is up). */
|
|
121
|
+
function createEntry(options: SidebarEntryOptions): { entry: HTMLElement; applyLabel: () => void; setOpen: (open: boolean) => void } {
|
|
122
|
+
const actions = options.actions ?? []
|
|
123
|
+
const composite = actions.length > 0
|
|
124
|
+
const entry = document.createElement(composite ? 'div' : 'button') as HTMLElement
|
|
125
|
+
if (!composite) (entry as HTMLButtonElement).type = 'button'
|
|
91
126
|
entry.setAttribute(options.rowAttribute, '')
|
|
92
127
|
if (options.plugin !== undefined) {
|
|
93
128
|
entry.setAttribute('data-dsh-plugin', options.plugin)
|
|
@@ -99,19 +134,66 @@ function createEntry(options: SidebarEntryOptions): { entry: HTMLButtonElement;
|
|
|
99
134
|
const iconSpan = document.createElement('span')
|
|
100
135
|
iconSpan.className = options.css['entryIcon'] ?? ''
|
|
101
136
|
iconSpan.innerHTML = options.icon
|
|
102
|
-
|
|
137
|
+
// The main hit area: the classic row IS the button; with trailing actions
|
|
138
|
+
// the row becomes a container and the main area its own button, so the row
|
|
139
|
+
// never nests interactive elements.
|
|
140
|
+
const main = composite ? document.createElement('button') : (entry as HTMLButtonElement)
|
|
141
|
+
if (composite) {
|
|
142
|
+
main.type = 'button'
|
|
143
|
+
main.className = options.css['entryMain'] ?? ''
|
|
144
|
+
main.append(iconSpan, labelSpan)
|
|
145
|
+
entry.append(main)
|
|
146
|
+
} else {
|
|
147
|
+
entry.append(iconSpan, labelSpan)
|
|
148
|
+
}
|
|
149
|
+
const actionButtons: { action: SidebarEntryAction; button: HTMLButtonElement }[] = []
|
|
150
|
+
for (const action of actions) {
|
|
151
|
+
const button = document.createElement('button')
|
|
152
|
+
button.type = 'button'
|
|
153
|
+
button.className = options.css['entryAction'] ?? ''
|
|
154
|
+
button.setAttribute('data-dsh-entry-action', action.id)
|
|
155
|
+
button.innerHTML = action.icon
|
|
156
|
+
button.addEventListener('click', () => { action.onClick(button) })
|
|
157
|
+
entry.append(button)
|
|
158
|
+
actionButtons.push({ action, button })
|
|
159
|
+
}
|
|
160
|
+
let open = false
|
|
161
|
+
const applyActions = (): void => {
|
|
162
|
+
for (const { action, button } of actionButtons) {
|
|
163
|
+
button.innerHTML = open || action.inactiveIcon === undefined ? action.icon : action.inactiveIcon
|
|
164
|
+
const text = action.label(open)
|
|
165
|
+
button.setAttribute('aria-label', text)
|
|
166
|
+
button.setAttribute('title', text)
|
|
167
|
+
if (action.inactiveIcon !== undefined) button.setAttribute('aria-expanded', String(open))
|
|
168
|
+
}
|
|
169
|
+
}
|
|
103
170
|
const applyLabel = (): void => {
|
|
104
|
-
|
|
105
|
-
if (options.tooltip !== undefined)
|
|
171
|
+
main.setAttribute('aria-label', options.label())
|
|
172
|
+
if (options.tooltip !== undefined) main.setAttribute('title', options.tooltip())
|
|
106
173
|
labelSpan.textContent = options.label()
|
|
174
|
+
applyActions()
|
|
107
175
|
}
|
|
108
176
|
applyLabel()
|
|
109
|
-
|
|
110
|
-
|
|
177
|
+
main.addEventListener('click', options.onToggle)
|
|
178
|
+
if (composite) {
|
|
179
|
+
// The old row was clickable edge to edge; keep the padding between the
|
|
180
|
+
// main area and the action buttons a toggle target as well.
|
|
181
|
+
entry.addEventListener('click', (event) => {
|
|
182
|
+
if (event.target === entry) options.onToggle()
|
|
183
|
+
})
|
|
184
|
+
}
|
|
185
|
+
return {
|
|
186
|
+
entry,
|
|
187
|
+
applyLabel,
|
|
188
|
+
setOpen: (next: boolean) => {
|
|
189
|
+
open = next
|
|
190
|
+
applyActions()
|
|
191
|
+
},
|
|
192
|
+
}
|
|
111
193
|
}
|
|
112
194
|
|
|
113
195
|
/** Re-insert the entry after the New Session row (before the browser region). */
|
|
114
|
-
function placeEntry(root: HTMLElement, entry:
|
|
196
|
+
function placeEntry(root: HTMLElement, entry: HTMLElement, options: SidebarEntryOptions): boolean {
|
|
115
197
|
const button = newSessionButton(root)
|
|
116
198
|
if (button === undefined) return false
|
|
117
199
|
if (entry.parentElement !== root) {
|
|
@@ -149,7 +231,7 @@ export function mountSidebarEntry(options: SidebarEntryOptions): () => void {
|
|
|
149
231
|
if (typeof document !== 'undefined' && document.querySelector(options.rowSelector) !== null) {
|
|
150
232
|
return () => {}
|
|
151
233
|
}
|
|
152
|
-
const { entry, applyLabel } = createEntry(options)
|
|
234
|
+
const { entry, applyLabel, setOpen } = createEntry(options)
|
|
153
235
|
let root: HTMLElement | undefined
|
|
154
236
|
let placed = false
|
|
155
237
|
let unsubscribeRefresh: (() => void) | undefined
|
|
@@ -217,8 +299,10 @@ export function mountSidebarEntry(options: SidebarEntryOptions): () => void {
|
|
|
217
299
|
// row permanently highlighted — delete the attribute instead.
|
|
218
300
|
const unsubscribeActive = options.active === undefined ? undefined : (() => {
|
|
219
301
|
const syncActive = (): void => {
|
|
220
|
-
|
|
302
|
+
const open = options.active!.isOpen()
|
|
303
|
+
if (open) entry.dataset.active = 'true'
|
|
221
304
|
else delete entry.dataset.active
|
|
305
|
+
setOpen(open)
|
|
222
306
|
}
|
|
223
307
|
const unsubscribe = options.active.subscribe(syncActive)
|
|
224
308
|
syncActive()
|