@rezti/dsh-rez-suite 0.1.4 → 0.1.5
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/README.md +1 -1
- package/README.zh.md +1 -1
- package/lib/client.d.ts +19 -152
- package/lib/client.js +156 -307
- package/lib/index.js +1 -1
- package/lib/style.css +82 -111
- package/package.json +8 -6
- package/src/client/index.ts +33 -21
- package/src/client/locales.ts +4 -0
- package/src/client/panel/AuditTab.tsx +6 -6
- package/src/client/panel/panel.module.css +89 -118
- package/src/client/settings-card.tsx +68 -0
- package/src/index.ts +4 -4
- package/src/client/mount.tsx +0 -97
- package/src/client/panel/RezPanel.tsx +0 -53
- package/src/client/panel/controller.ts +0 -42
- package/src/client/sidebar-entry.ts +0 -80
|
@@ -1,117 +1,89 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* Settings-native styles for the ReZ card/tab. Colors ride dsh --dsw-* tokens
|
|
3
|
+
* so the form matches 设置 → 插件. The stylesheet is inlined into client.js
|
|
4
|
+
* at wrap time — dsh only loads the JS module, not lib/style.css.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
.shell {
|
|
8
|
+
display: flex;
|
|
9
|
+
flex-direction: column;
|
|
10
|
+
gap: 12px;
|
|
11
|
+
min-width: 0;
|
|
9
12
|
}
|
|
10
13
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
.card {
|
|
15
|
+
list-style: none;
|
|
16
|
+
border: 1px solid var(--dsw-alias-border-l2);
|
|
17
|
+
border-radius: 12px;
|
|
18
|
+
background: var(--dsw-alias-bg-layer-3);
|
|
19
|
+
transition: border-color 0.16s, background 0.16s;
|
|
17
20
|
}
|
|
18
21
|
|
|
19
|
-
|
|
20
|
-
|
|
22
|
+
.card:hover {
|
|
23
|
+
border-color: var(--dsw-alias-label-dimmed);
|
|
21
24
|
}
|
|
22
25
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
+
.cardOpen {
|
|
27
|
+
background: var(--dsw-alias-bg-layer-2);
|
|
28
|
+
border-color: var(--dsw-alias-label-dimmed);
|
|
26
29
|
}
|
|
27
30
|
|
|
28
|
-
.
|
|
29
|
-
display: flex;
|
|
30
|
-
align-items: center;
|
|
31
|
-
gap: 8px;
|
|
31
|
+
.header {
|
|
32
32
|
width: 100%;
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
background:
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
33
|
+
appearance: none;
|
|
34
|
+
border: 0;
|
|
35
|
+
background: none;
|
|
36
|
+
font: inherit;
|
|
37
|
+
color: inherit;
|
|
38
|
+
text-align: left;
|
|
39
39
|
cursor: pointer;
|
|
40
|
-
|
|
41
|
-
white-space: nowrap;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
.entry:hover {
|
|
45
|
-
background: var(--dsw-specific-sidebar-nav-item-hover);
|
|
46
|
-
color: var(--dsw-alias-label-primary);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
.entryIcon {
|
|
50
|
-
display: inline-flex;
|
|
40
|
+
display: flex;
|
|
51
41
|
align-items: center;
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
.entryLabel {
|
|
57
|
-
overflow: hidden;
|
|
58
|
-
text-overflow: ellipsis;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
[data-dsh-frame][data-sidebar-collapsed] .entry {
|
|
62
|
-
justify-content: center;
|
|
63
|
-
padding: 0;
|
|
64
|
-
width: 100%;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
[data-dsh-frame][data-sidebar-collapsed] .entryLabel {
|
|
68
|
-
display: none;
|
|
42
|
+
gap: 12px;
|
|
43
|
+
padding: 14px 16px;
|
|
44
|
+
border-radius: 12px;
|
|
69
45
|
}
|
|
70
46
|
|
|
71
|
-
.
|
|
72
|
-
|
|
47
|
+
.header:focus-visible {
|
|
48
|
+
outline: 2px solid var(--dsw-alias-brand-primary);
|
|
49
|
+
outline-offset: -2px;
|
|
73
50
|
}
|
|
74
51
|
|
|
75
|
-
.
|
|
52
|
+
.headText {
|
|
53
|
+
flex: 1;
|
|
54
|
+
min-width: 0;
|
|
76
55
|
display: flex;
|
|
77
56
|
flex-direction: column;
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
57
|
+
gap: 4px;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.name {
|
|
61
|
+
font-size: 15px;
|
|
62
|
+
font-weight: 600;
|
|
63
|
+
line-height: 1.4;
|
|
84
64
|
color: var(--dsw-alias-label-primary);
|
|
85
|
-
font-family: var(--dsw-font-family);
|
|
86
65
|
}
|
|
87
66
|
|
|
88
|
-
.
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
flex: none;
|
|
67
|
+
.description {
|
|
68
|
+
font-size: 13px;
|
|
69
|
+
line-height: 1.5;
|
|
70
|
+
color: var(--dsw-alias-label-tertiary);
|
|
93
71
|
}
|
|
94
72
|
|
|
95
|
-
.
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
font-weight: 700;
|
|
73
|
+
.chevron {
|
|
74
|
+
flex: none;
|
|
75
|
+
color: var(--dsw-alias-label-tertiary);
|
|
76
|
+
transition: transform 0.16s;
|
|
100
77
|
}
|
|
101
78
|
|
|
102
|
-
.
|
|
103
|
-
|
|
104
|
-
background: transparent;
|
|
105
|
-
color: var(--dsw-alias-label-secondary);
|
|
106
|
-
cursor: pointer;
|
|
107
|
-
font-size: 14px;
|
|
108
|
-
padding: 4px 8px;
|
|
109
|
-
border-radius: 6px;
|
|
79
|
+
.chevronOpen {
|
|
80
|
+
transform: rotate(180deg);
|
|
110
81
|
}
|
|
111
82
|
|
|
112
|
-
.
|
|
113
|
-
|
|
114
|
-
|
|
83
|
+
.body {
|
|
84
|
+
border-top: 1px solid var(--dsw-alias-border-l2);
|
|
85
|
+
margin: 0 16px 12px;
|
|
86
|
+
padding-top: 8px;
|
|
115
87
|
}
|
|
116
88
|
|
|
117
89
|
.tabBar {
|
|
@@ -143,21 +115,12 @@ html[data-dsh-rez-active]:not([data-dsh-taskboard-active]):not([data-dsh-ssh-act
|
|
|
143
115
|
border-bottom-color: var(--dsw-alias-state-business-primary);
|
|
144
116
|
}
|
|
145
117
|
|
|
146
|
-
.panelContent {
|
|
147
|
-
flex: 1;
|
|
148
|
-
min-height: 0;
|
|
149
|
-
display: flex;
|
|
150
|
-
flex-direction: column;
|
|
151
|
-
overflow: hidden;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
118
|
.tabBody {
|
|
155
119
|
display: flex;
|
|
156
120
|
flex-direction: column;
|
|
157
121
|
gap: 12px;
|
|
158
|
-
|
|
159
|
-
min-
|
|
160
|
-
overflow-y: auto;
|
|
122
|
+
padding: 12px 0 4px;
|
|
123
|
+
min-width: 0;
|
|
161
124
|
}
|
|
162
125
|
|
|
163
126
|
.section {
|
|
@@ -174,13 +137,14 @@ html[data-dsh-rez-active]:not([data-dsh-taskboard-active]):not([data-dsh-ssh-act
|
|
|
174
137
|
margin: 0;
|
|
175
138
|
font-size: 13px;
|
|
176
139
|
font-weight: 600;
|
|
140
|
+
color: var(--dsw-alias-label-primary);
|
|
177
141
|
}
|
|
178
142
|
|
|
179
143
|
.field {
|
|
180
|
-
display:
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
144
|
+
display: flex;
|
|
145
|
+
flex-direction: column;
|
|
146
|
+
align-items: stretch;
|
|
147
|
+
gap: 4px;
|
|
184
148
|
}
|
|
185
149
|
|
|
186
150
|
.label {
|
|
@@ -188,11 +152,12 @@ html[data-dsh-rez-active]:not([data-dsh-taskboard-active]):not([data-dsh-ssh-act
|
|
|
188
152
|
color: var(--dsw-alias-label-secondary);
|
|
189
153
|
}
|
|
190
154
|
|
|
191
|
-
.input,
|
|
155
|
+
.input,
|
|
156
|
+
.select {
|
|
192
157
|
padding: 6px 8px;
|
|
193
158
|
font-size: 13px;
|
|
194
159
|
color: var(--dsw-alias-label-primary);
|
|
195
|
-
background: var(--dsw-specific-input-major);
|
|
160
|
+
background: var(--dsw-specific-input-major, var(--dsw-alias-bg-layer-3));
|
|
196
161
|
border: 1px solid var(--dsw-alias-border-l2);
|
|
197
162
|
border-radius: 8px;
|
|
198
163
|
outline: none;
|
|
@@ -217,28 +182,24 @@ html[data-dsh-rez-active]:not([data-dsh-taskboard-active]):not([data-dsh-ssh-act
|
|
|
217
182
|
font-size: 13px;
|
|
218
183
|
border-radius: 8px;
|
|
219
184
|
border: 1px solid var(--dsw-alias-border-l2);
|
|
220
|
-
background: var(--dsw-specific-input-major);
|
|
185
|
+
background: var(--dsw-specific-input-major, var(--dsw-alias-bg-layer-3));
|
|
221
186
|
color: var(--dsw-alias-label-primary);
|
|
222
187
|
cursor: pointer;
|
|
223
188
|
}
|
|
224
189
|
|
|
225
|
-
.button:hover {
|
|
190
|
+
.button:hover:not(:disabled) {
|
|
226
191
|
background: var(--dsw-alias-interactive-bg-hover);
|
|
227
192
|
}
|
|
228
193
|
|
|
229
|
-
.
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
height: 240px;
|
|
233
|
-
margin: 8px 0;
|
|
234
|
-
background: #fff;
|
|
235
|
-
border-radius: 8px;
|
|
194
|
+
.button:disabled {
|
|
195
|
+
opacity: 0.4;
|
|
196
|
+
cursor: default;
|
|
236
197
|
}
|
|
237
198
|
|
|
238
199
|
.primaryButton {
|
|
239
|
-
background: var(--dsw-alias-
|
|
240
|
-
border-color: var(--dsw-alias-
|
|
241
|
-
color:
|
|
200
|
+
background: var(--dsw-alias-label-primary);
|
|
201
|
+
border-color: var(--dsw-alias-label-primary);
|
|
202
|
+
color: var(--dsw-alias-bg-layer-3);
|
|
242
203
|
}
|
|
243
204
|
|
|
244
205
|
.dangerButton {
|
|
@@ -247,8 +208,19 @@ html[data-dsh-rez-active]:not([data-dsh-taskboard-active]):not([data-dsh-ssh-act
|
|
|
247
208
|
color: #fff;
|
|
248
209
|
}
|
|
249
210
|
|
|
211
|
+
.qr {
|
|
212
|
+
display: block;
|
|
213
|
+
width: 200px;
|
|
214
|
+
height: 200px;
|
|
215
|
+
margin: 8px 0;
|
|
216
|
+
background: #fff;
|
|
217
|
+
border-radius: 8px;
|
|
218
|
+
}
|
|
219
|
+
|
|
250
220
|
.message {
|
|
221
|
+
margin: 0;
|
|
251
222
|
font-size: 12.5px;
|
|
223
|
+
line-height: 1.5;
|
|
252
224
|
color: var(--dsw-alias-label-secondary);
|
|
253
225
|
}
|
|
254
226
|
|
|
@@ -257,8 +229,6 @@ html[data-dsh-rez-active]:not([data-dsh-taskboard-active]):not([data-dsh-ssh-act
|
|
|
257
229
|
}
|
|
258
230
|
|
|
259
231
|
.tableWrap {
|
|
260
|
-
flex: 1;
|
|
261
|
-
min-height: 0;
|
|
262
232
|
overflow: auto;
|
|
263
233
|
border: 1px solid var(--dsw-alias-border-l1);
|
|
264
234
|
border-radius: 10px;
|
|
@@ -304,11 +274,11 @@ html[data-dsh-rez-active]:not([data-dsh-taskboard-active]):not([data-dsh-ssh-act
|
|
|
304
274
|
|
|
305
275
|
.cards {
|
|
306
276
|
display: grid;
|
|
307
|
-
grid-template-columns: repeat(auto-fill, minmax(
|
|
277
|
+
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
|
|
308
278
|
gap: 8px;
|
|
309
279
|
}
|
|
310
280
|
|
|
311
|
-
.
|
|
281
|
+
.stat {
|
|
312
282
|
padding: 10px;
|
|
313
283
|
border: 1px solid var(--dsw-alias-border-l1);
|
|
314
284
|
border-radius: 10px;
|
|
@@ -322,4 +292,5 @@ html[data-dsh-rez-active]:not([data-dsh-taskboard-active]):not([data-dsh-ssh-act
|
|
|
322
292
|
.cardValue {
|
|
323
293
|
font-size: 15px;
|
|
324
294
|
font-weight: 600;
|
|
295
|
+
color: var(--dsw-alias-label-primary);
|
|
325
296
|
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ReZ settings pages: one accordion card in 设置 → 插件 → 插件配置,
|
|
3
|
+
* plus a dedicated Plugins tab for the same content.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { useMemo, useState } from 'react'
|
|
7
|
+
import { RezApi } from './api.ts'
|
|
8
|
+
import { tt } from './panel/helpers.ts'
|
|
9
|
+
import { AuditTab } from './panel/AuditTab.tsx'
|
|
10
|
+
import { ConfigTab } from './panel/ConfigTab.tsx'
|
|
11
|
+
import { StatusTab } from './panel/StatusTab.tsx'
|
|
12
|
+
import { WeixinTab } from './panel/WeixinTab.tsx'
|
|
13
|
+
import css from './panel/panel.module.css'
|
|
14
|
+
|
|
15
|
+
export type RezTab = 'config' | 'weixin' | 'status' | 'audit'
|
|
16
|
+
|
|
17
|
+
const TABS: ReadonlyArray<{ id: RezTab; label: () => string }> = [
|
|
18
|
+
{ id: 'config', label: () => tt('tab.config') },
|
|
19
|
+
{ id: 'weixin', label: () => tt('tab.weixin') },
|
|
20
|
+
{ id: 'status', label: () => tt('tab.status') },
|
|
21
|
+
{ id: 'audit', label: () => tt('tab.audit') },
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
export function RezSettingsPage() {
|
|
25
|
+
const api = useMemo(() => new RezApi(), [])
|
|
26
|
+
const [activeTab, setActiveTab] = useState<RezTab>('config')
|
|
27
|
+
return (
|
|
28
|
+
<div className={css.shell}>
|
|
29
|
+
<div className={css.tabBar} role="tablist">
|
|
30
|
+
{TABS.map(tab => (
|
|
31
|
+
<button
|
|
32
|
+
key={tab.id}
|
|
33
|
+
type="button"
|
|
34
|
+
role="tab"
|
|
35
|
+
aria-selected={activeTab === tab.id}
|
|
36
|
+
data-active={activeTab === tab.id ? '' : undefined}
|
|
37
|
+
className={css.tab}
|
|
38
|
+
onClick={() => { setActiveTab(tab.id) }}
|
|
39
|
+
>
|
|
40
|
+
{tab.label()}
|
|
41
|
+
</button>
|
|
42
|
+
))}
|
|
43
|
+
</div>
|
|
44
|
+
{activeTab === 'config' && <ConfigTab api={api} />}
|
|
45
|
+
{activeTab === 'weixin' && <WeixinTab api={api} />}
|
|
46
|
+
{activeTab === 'status' && <StatusTab api={api} />}
|
|
47
|
+
{activeTab === 'audit' && <AuditTab api={api} />}
|
|
48
|
+
</div>
|
|
49
|
+
)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function RezPluginCard() {
|
|
53
|
+
const [open, setOpen] = useState(false)
|
|
54
|
+
return (
|
|
55
|
+
<li className={css.card + (open ? ' ' + css.cardOpen : '')}>
|
|
56
|
+
<button type="button" className={css.header} onClick={() => { setOpen(!open) }}>
|
|
57
|
+
<div className={css.headText}>
|
|
58
|
+
<div className={css.name}>{tt('settings.title')}</div>
|
|
59
|
+
<div className={css.description}>{tt('settings.description')}</div>
|
|
60
|
+
</div>
|
|
61
|
+
<svg className={css.chevron + (open ? ' ' + css.chevronOpen : '')} viewBox="0 0 14 14" width="14" height="14" aria-hidden="true">
|
|
62
|
+
<path d="M3 5l4 4 4-4" fill="none" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round" />
|
|
63
|
+
</svg>
|
|
64
|
+
</button>
|
|
65
|
+
{open ? <div className={css.body}><RezSettingsPage /></div> : null}
|
|
66
|
+
</li>
|
|
67
|
+
)
|
|
68
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
* dsh-rez-suite — employee-facing host half.
|
|
3
3
|
*
|
|
4
4
|
* MCP connections are owned by official @deepseek-ai/dsh-mcp-client rows in
|
|
5
|
-
* this package's cordis.patch.yml. This plugin only mounts the
|
|
6
|
-
*
|
|
7
|
-
* start a second MCP host here.
|
|
5
|
+
* this package's cordis.patch.yml. This plugin only mounts the loopback
|
|
6
|
+
* settings API, a Settings → Plugins card, and a short system-prompt
|
|
7
|
+
* announcement. Do not start a second MCP host here.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import type { Context } from '@deepseek-ai/cordis'
|
|
@@ -60,7 +60,7 @@ export const REZ_GUIDANCE = [
|
|
|
60
60
|
'本机已安装 ReZ-TI 套件(一次安装):工作身份由 dsh-rez-sso 提供,MCP 由官方 dsh-mcp-client 接入 Odoo / Nextcloud / 企业微信 / Home Assistant。',
|
|
61
61
|
'工具名 mcp__odoo__*、mcp__nextcloud__*、mcp__wechat__*、mcp__homeassistant__*。',
|
|
62
62
|
'密钥:REZ_ODOO_TOKEN、REZ_NEXTCLOUD_PASSWORD、REZ_WECHAT_WEBHOOK、REZ_HA_TOKEN。',
|
|
63
|
-
'个人微信(QClaw/ClawBot):打开
|
|
63
|
+
'个人微信(QClaw/ClawBot):打开 设置 → 插件 → ReZ-TI → 微信 扫码,然后直接在微信里说话,不必再跑命令、也不要调用微信 MCP 工具。',
|
|
64
64
|
'意图用 rez_set_intent(erp/files/chat/home/auto)。审计用 rez_audit。',
|
|
65
65
|
'涉及资金、对外发送、设备控制、生产写入时先征得用户批准。',
|
|
66
66
|
].join('')
|
package/src/client/mount.tsx
DELETED
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Rez panel view mounting. Single-occupant center-column takeover, same
|
|
3
|
-
* mechanism as the task board and ssh panels. The conversation subtree stays
|
|
4
|
-
* mounted under the panel.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import { createRoot, type Root } from 'react-dom/client'
|
|
8
|
-
import type { RezApi } from './api.ts'
|
|
9
|
-
import type { PanelController } from './panel/controller.ts'
|
|
10
|
-
import { RezPanel } from './panel/RezPanel.tsx'
|
|
11
|
-
import css from './panel/panel.module.css'
|
|
12
|
-
|
|
13
|
-
export const PANEL_VIEW_SELECTOR = '[data-dsh-rez-view]'
|
|
14
|
-
|
|
15
|
-
const CONVERSATION_COLUMN_SELECTOR = '[data-pane="conversation"]'
|
|
16
|
-
const ACTIVE_ATTR = 'data-dsh-rez-active'
|
|
17
|
-
const OTHER_ACTIVE_ATTRS = ['data-dsh-taskboard-active', 'data-dsh-ssh-active']
|
|
18
|
-
const ACTIVATE_EVENT = 'dsh-panel-activate'
|
|
19
|
-
const PANEL_NAME = 'rez'
|
|
20
|
-
|
|
21
|
-
function conversationColumn(): HTMLElement | undefined {
|
|
22
|
-
return document.querySelector<HTMLElement>(CONVERSATION_COLUMN_SELECTOR) ?? undefined
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export function mountPanel(controller: PanelController, api: RezApi): () => void {
|
|
26
|
-
let root: Root | undefined
|
|
27
|
-
let container: HTMLDivElement | undefined
|
|
28
|
-
|
|
29
|
-
const ensure = (): void => {
|
|
30
|
-
if (container !== undefined) {
|
|
31
|
-
if (container.isConnected) return
|
|
32
|
-
root?.unmount()
|
|
33
|
-
root = undefined
|
|
34
|
-
container.remove()
|
|
35
|
-
container = undefined
|
|
36
|
-
}
|
|
37
|
-
const column = conversationColumn()
|
|
38
|
-
if (column === undefined) return
|
|
39
|
-
container = document.createElement('div')
|
|
40
|
-
container.dataset.dshRezView = ''
|
|
41
|
-
container.className = css.view
|
|
42
|
-
column.appendChild(container)
|
|
43
|
-
root = createRoot(container)
|
|
44
|
-
root.render(<RezPanel controller={controller} api={api} />)
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
const waitObserver = new MutationObserver(() => { ensure() })
|
|
48
|
-
waitObserver.observe(document.body, { childList: true, subtree: true })
|
|
49
|
-
|
|
50
|
-
// The shell may mount the center column after this plugin applies. Retry
|
|
51
|
-
// until the container is attached; ensure() is idempotent once connected.
|
|
52
|
-
const retryTimer = window.setInterval(() => {
|
|
53
|
-
ensure()
|
|
54
|
-
if (container?.isConnected === true) window.clearInterval(retryTimer)
|
|
55
|
-
}, 250)
|
|
56
|
-
|
|
57
|
-
const applyActive = (): void => {
|
|
58
|
-
if (controller.getSnapshot().panelOpen) {
|
|
59
|
-
for (const attr of OTHER_ACTIVE_ATTRS) document.documentElement.removeAttribute(attr)
|
|
60
|
-
document.documentElement.setAttribute(ACTIVE_ATTR, '')
|
|
61
|
-
document.dispatchEvent(new CustomEvent(ACTIVATE_EVENT, { detail: PANEL_NAME }))
|
|
62
|
-
} else {
|
|
63
|
-
document.documentElement.removeAttribute(ACTIVE_ATTR)
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
const onOtherActivate = (event: Event): void => {
|
|
67
|
-
const detail = (event as CustomEvent).detail
|
|
68
|
-
if ((detail === 'taskboard' || detail === 'ssh') && controller.getSnapshot().panelOpen) {
|
|
69
|
-
controller.close()
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
const SIDEBAR_ROW_SELECTOR = '[class*="sessionRow"], [class*="projectRow"], [class*="searchResultRow"], [class*="searchResultWorkspace"], [class*="newSession"]'
|
|
73
|
-
const onClickSidebarRow = (event: MouseEvent): void => {
|
|
74
|
-
if (!controller.getSnapshot().panelOpen) return
|
|
75
|
-
const target = event.target as HTMLElement | null
|
|
76
|
-
if (target === null) return
|
|
77
|
-
if (target.closest(SIDEBAR_ROW_SELECTOR) !== null) controller.close()
|
|
78
|
-
}
|
|
79
|
-
document.addEventListener('click', onClickSidebarRow, true)
|
|
80
|
-
document.addEventListener(ACTIVATE_EVENT, onOtherActivate)
|
|
81
|
-
const unsubscribe = controller.subscribe(applyActive)
|
|
82
|
-
applyActive()
|
|
83
|
-
ensure()
|
|
84
|
-
|
|
85
|
-
return () => {
|
|
86
|
-
window.clearInterval(retryTimer)
|
|
87
|
-
document.removeEventListener('click', onClickSidebarRow, true)
|
|
88
|
-
document.removeEventListener(ACTIVATE_EVENT, onOtherActivate)
|
|
89
|
-
waitObserver.disconnect()
|
|
90
|
-
unsubscribe()
|
|
91
|
-
document.documentElement.removeAttribute(ACTIVE_ATTR)
|
|
92
|
-
root?.unmount()
|
|
93
|
-
root = undefined
|
|
94
|
-
container?.remove()
|
|
95
|
-
container = undefined
|
|
96
|
-
}
|
|
97
|
-
}
|
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Rez Suite panel shell: header + three tabs (config/status/audit).
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { useState } from 'react'
|
|
6
|
-
import type { RezApi } from '../api.ts'
|
|
7
|
-
import type { PanelController } from './controller.ts'
|
|
8
|
-
import { tt } from './helpers.ts'
|
|
9
|
-
import { AuditTab } from './AuditTab.tsx'
|
|
10
|
-
import { ConfigTab } from './ConfigTab.tsx'
|
|
11
|
-
import { StatusTab } from './StatusTab.tsx'
|
|
12
|
-
import { WeixinTab } from './WeixinTab.tsx'
|
|
13
|
-
import css from './panel.module.css'
|
|
14
|
-
|
|
15
|
-
export type RezTab = 'config' | 'weixin' | 'status' | 'audit'
|
|
16
|
-
|
|
17
|
-
export interface RezPanelProps {
|
|
18
|
-
controller: PanelController
|
|
19
|
-
api: RezApi
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
const TABS: ReadonlyArray<{ id: RezTab; label: () => string }> = [
|
|
23
|
-
{ id: 'config', label: () => tt('tab.config') },
|
|
24
|
-
{ id: 'weixin', label: () => tt('tab.weixin') },
|
|
25
|
-
{ id: 'status', label: () => tt('tab.status') },
|
|
26
|
-
{ id: 'audit', label: () => tt('tab.audit') },
|
|
27
|
-
]
|
|
28
|
-
|
|
29
|
-
export function RezPanel({ controller, api }: RezPanelProps) {
|
|
30
|
-
const [activeTab, setActiveTab] = useState<RezTab>('config')
|
|
31
|
-
|
|
32
|
-
return (
|
|
33
|
-
<div className={css.panel}>
|
|
34
|
-
<div className={css.panelHeader}>
|
|
35
|
-
<h2 className={css.panelTitle}>{tt('panel.title')}</h2>
|
|
36
|
-
<button type="button" className={css.iconButton} title={tt('common.close')} aria-label={tt('common.close')} onClick={() => { controller.close() }}>x</button>
|
|
37
|
-
</div>
|
|
38
|
-
<div className={css.tabBar} role="tablist">
|
|
39
|
-
{TABS.map(tab => (
|
|
40
|
-
<button key={tab.id} type="button" role="tab" aria-selected={activeTab === tab.id} data-active={activeTab === tab.id ? '' : undefined} className={css.tab} onClick={() => { setActiveTab(tab.id) }}>
|
|
41
|
-
{tab.label()}
|
|
42
|
-
</button>
|
|
43
|
-
))}
|
|
44
|
-
</div>
|
|
45
|
-
<div className={css.panelContent}>
|
|
46
|
-
{activeTab === 'config' && <ConfigTab api={api} />}
|
|
47
|
-
{activeTab === 'weixin' && <WeixinTab api={api} />}
|
|
48
|
-
{activeTab === 'status' && <StatusTab api={api} />}
|
|
49
|
-
{activeTab === 'audit' && <AuditTab api={api} />}
|
|
50
|
-
</div>
|
|
51
|
-
</div>
|
|
52
|
-
)
|
|
53
|
-
}
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Rez panel controller: single owner of open/closed state (browser session).
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
export interface PanelControllerSnapshot {
|
|
6
|
-
panelOpen: boolean
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export class PanelController {
|
|
10
|
-
private panelOpen = false
|
|
11
|
-
private listeners = new Set<() => void>()
|
|
12
|
-
|
|
13
|
-
getSnapshot(): PanelControllerSnapshot {
|
|
14
|
-
return { panelOpen: this.panelOpen }
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
subscribe(fn: () => void): () => void {
|
|
18
|
-
this.listeners.add(fn)
|
|
19
|
-
return () => { this.listeners.delete(fn) }
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
open(): void {
|
|
23
|
-
if (this.panelOpen) return
|
|
24
|
-
this.panelOpen = true
|
|
25
|
-
this.notify()
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
close(): void {
|
|
29
|
-
if (!this.panelOpen) return
|
|
30
|
-
this.panelOpen = false
|
|
31
|
-
this.notify()
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
toggle(): void {
|
|
35
|
-
if (this.panelOpen) this.close()
|
|
36
|
-
else this.open()
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
private notify(): void {
|
|
40
|
-
for (const fn of [...this.listeners]) fn()
|
|
41
|
-
}
|
|
42
|
-
}
|
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Sidebar entry injection (DOM-level extension, task-board/ssh precedent).
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import type { PanelController } from './panel/controller.ts'
|
|
6
|
-
import { tt } from './panel/helpers.ts'
|
|
7
|
-
import css from './panel/panel.module.css'
|
|
8
|
-
|
|
9
|
-
export const ENTRY_SELECTOR = '[data-dsh-rez-entry]'
|
|
10
|
-
|
|
11
|
-
const ICON = '<svg viewBox="0 0 16 16" width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="2.5" y="2.5" width="11" height="11" rx="2"/><path d="M5 8l2 2 4-4"/></svg>'
|
|
12
|
-
|
|
13
|
-
function sidebarRoot(): HTMLElement | undefined {
|
|
14
|
-
const column = document.querySelector<HTMLElement>('[data-pane="sidebar"], [class*="sidebarCol"]')
|
|
15
|
-
if (column === null) return undefined
|
|
16
|
-
const logoOwner = column.querySelector<HTMLElement>('[class*="logoRow"]')?.parentElement
|
|
17
|
-
return logoOwner ?? (column.firstElementChild as HTMLElement | undefined)
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
function newSessionButton(root: HTMLElement): HTMLButtonElement | undefined {
|
|
21
|
-
const nested = root.querySelector<HTMLButtonElement>('button[class*="newSession"]')
|
|
22
|
-
if (nested !== null) return nested
|
|
23
|
-
for (const child of root.children) {
|
|
24
|
-
if (child.tagName === 'BUTTON') return child as HTMLButtonElement
|
|
25
|
-
}
|
|
26
|
-
return undefined
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function createEntry(controller: PanelController): HTMLButtonElement {
|
|
30
|
-
const entry = document.createElement('button')
|
|
31
|
-
entry.type = 'button'
|
|
32
|
-
entry.dataset.dshRezEntry = ''
|
|
33
|
-
entry.className = css.entry
|
|
34
|
-
entry.setAttribute('aria-label', tt('entry.label'))
|
|
35
|
-
entry.setAttribute('title', tt('entry.tooltip'))
|
|
36
|
-
entry.innerHTML = '<span class="' + css.entryIcon + '">' + ICON + '</span><span class="' + css.entryLabel + '">' + tt('entry.label') + '</span>'
|
|
37
|
-
entry.addEventListener('click', () => { controller.toggle() })
|
|
38
|
-
return entry
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function placeEntry(root: HTMLElement, entry: HTMLButtonElement): boolean {
|
|
42
|
-
const button = newSessionButton(root)
|
|
43
|
-
if (button === undefined) return false
|
|
44
|
-
if (entry.parentElement !== root) {
|
|
45
|
-
const row = button.closest('[class*="logoRow"]')
|
|
46
|
-
const base = (row !== null && row.parentElement === root) ? row : button
|
|
47
|
-
const family = Array.from(root.children).filter(
|
|
48
|
-
(el): el is HTMLElement => el instanceof HTMLElement && el.matches('[data-dsh-taskboard-entry], [data-dsh-ssh-entry], [data-dsh-rez-entry]'),
|
|
49
|
-
)
|
|
50
|
-
const anchor = family.length > 0 ? family[family.length - 1].nextElementSibling : base.nextElementSibling
|
|
51
|
-
root.insertBefore(entry, anchor)
|
|
52
|
-
}
|
|
53
|
-
return true
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
export function mountSidebarEntry(controller: PanelController): () => void {
|
|
57
|
-
const entry = createEntry(controller)
|
|
58
|
-
let root: HTMLElement | undefined
|
|
59
|
-
let placed = false
|
|
60
|
-
|
|
61
|
-
const place = (): void => {
|
|
62
|
-
const next = sidebarRoot()
|
|
63
|
-
if (next === undefined) return
|
|
64
|
-
if (root !== undefined && root !== next) {
|
|
65
|
-
if (entry.parentElement === root) entry.remove()
|
|
66
|
-
placed = false
|
|
67
|
-
}
|
|
68
|
-
root = next
|
|
69
|
-
if (!placed) placed = placeEntry(root, entry)
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
const observer = new MutationObserver(place)
|
|
73
|
-
observer.observe(document.body, { childList: true, subtree: true })
|
|
74
|
-
place()
|
|
75
|
-
|
|
76
|
-
return () => {
|
|
77
|
-
observer.disconnect()
|
|
78
|
-
entry.remove()
|
|
79
|
-
}
|
|
80
|
-
}
|