@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.
@@ -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
- }