@rezti/dsh-rez-suite 0.1.4 → 0.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.
- package/README.md +1 -1
- package/README.zh.md +1 -1
- package/lib/client.d.ts +29 -155
- package/lib/client.js +365 -468
- package/lib/index.d.ts +20 -0
- package/lib/index.js +92 -13
- package/lib/style.css +82 -111
- package/package.json +9 -7
- package/src/client/index.ts +33 -21
- package/src/client/locales.ts +25 -7
- package/src/client/panel/AuditTab.tsx +6 -6
- package/src/client/panel/ConfigTab.tsx +141 -86
- package/src/client/panel/panel.module.css +89 -118
- package/src/client/settings-card.tsx +68 -0
- package/src/index.ts +37 -11
- package/src/protocol.ts +23 -0
- package/src/routes.ts +32 -4
- package/src/store.ts +37 -1
- 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
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
|
-
}
|