dsh-tiddlywiki 0.2.0 → 0.3.0

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.
@@ -79,6 +79,7 @@ interface StatusPayload {
79
79
  wikiPath?: string
80
80
  error?: string
81
81
  note?: { tag?: string }
82
+ ui?: { showPanelStatus?: boolean }
82
83
  }
83
84
 
84
85
  function conversationColumn(): HTMLElement | undefined {
@@ -115,6 +116,8 @@ export function mountPanel(state: PanelState): () => void {
115
116
  let frameArea: HTMLDivElement | undefined
116
117
  let errorArea: HTMLDivElement | undefined
117
118
  let chip: HTMLSpanElement | undefined
119
+ let statusDot: HTMLSpanElement | undefined
120
+ let statusFloater: HTMLDivElement | undefined
118
121
  let refreshTimer: number | undefined
119
122
  let refreshAttempts = 0
120
123
 
@@ -123,27 +126,6 @@ export function mountPanel(state: PanelState): () => void {
123
126
  view.dataset.dshTwView = ''
124
127
  view.className = 'dsh-tw-view'
125
128
 
126
- const bar = document.createElement('div')
127
- bar.className = 'dsh-tw-panel-bar'
128
- const title = document.createElement('span')
129
- title.className = 'dsh-tw-panel-title'
130
- title.textContent = 'TiddlyWiki 知识库'
131
- chip = document.createElement('span')
132
- chip.className = 'dsh-tw-status-chip'
133
- chip.dataset.state = 'unknown'
134
- chip.textContent = '—'
135
- const reload = document.createElement('button')
136
- reload.type = 'button'
137
- reload.textContent = '重载'
138
- reload.addEventListener('click', () => {
139
- if (iframe !== undefined && !iframe.hidden) iframe.src = iframe.src
140
- })
141
- const refresh = document.createElement('button')
142
- refresh.type = 'button'
143
- refresh.textContent = '状态'
144
- refresh.addEventListener('click', () => { void doRefresh() })
145
- bar.append(title, chip, refresh, reload)
146
-
147
129
  frameArea = document.createElement('div')
148
130
  frameArea.className = 'dsh-tw-panel-frame-wrap'
149
131
  frameArea.style.cssText = 'flex:1;min-height:0;display:flex;flex-direction:column'
@@ -157,14 +139,50 @@ export function mountPanel(state: PanelState): () => void {
157
139
  errorArea.className = 'dsh-tw-panel-error'
158
140
  errorArea.hidden = true
159
141
 
160
- view.append(bar, frameArea, errorArea)
142
+ view.append(frameArea, errorArea)
161
143
  return view
162
144
  }
163
145
 
146
+ /**
147
+ * Bottom-right floating status/reload buttons (same visual language as the
148
+ * quick-note toggle) — the top panel bar is gone, so TW's menubar reaches
149
+ * the very top of the panel and clears the host's top-right buttons. Mount
150
+ * is gated by `ui.showPanelStatus` (settings page toggle).
151
+ */
152
+ const buildStatusFloater = (): HTMLDivElement => {
153
+ const wrap = document.createElement('div')
154
+ wrap.className = 'dsh-tw-panel-status'
155
+
156
+ const statusBtn = document.createElement('button')
157
+ statusBtn.type = 'button'
158
+ statusBtn.className = 'dsh-tw-panel-status-btn'
159
+ statusBtn.title = '刷新状态'
160
+ statusDot = document.createElement('span')
161
+ statusDot.className = 'dsh-tw-status-dot'
162
+ statusDot.dataset.state = 'unknown'
163
+ chip = document.createElement('span')
164
+ chip.className = 'dsh-tw-panel-status-text'
165
+ chip.textContent = '—'
166
+ statusBtn.append(statusDot, chip)
167
+ statusBtn.addEventListener('click', () => { void doRefresh() })
168
+
169
+ const reloadBtn = document.createElement('button')
170
+ reloadBtn.type = 'button'
171
+ reloadBtn.className = 'dsh-tw-panel-status-btn'
172
+ reloadBtn.textContent = '重载'
173
+ reloadBtn.title = '重载 TiddlyWiki 面板'
174
+ reloadBtn.addEventListener('click', () => {
175
+ if (iframe !== undefined && !iframe.hidden) iframe.src = iframe.src
176
+ })
177
+
178
+ wrap.append(statusBtn, reloadBtn)
179
+ document.body.append(wrap)
180
+ return wrap
181
+ }
182
+
164
183
  const setChip = (stateName: string, text: string): void => {
165
- if (chip === undefined) return
166
- chip.dataset.state = stateName
167
- chip.textContent = text
184
+ if (statusDot !== undefined) statusDot.dataset.state = stateName
185
+ if (chip !== undefined) chip.textContent = text
168
186
  }
169
187
 
170
188
  /** Pin the overlay to the center column's current viewport rect. */
@@ -329,6 +347,13 @@ export function mountPanel(state: PanelState): () => void {
329
347
  ensure()
330
348
  applyActive()
331
349
 
350
+ // Bottom-right status/reload buttons, gated by ui.showPanelStatus.
351
+ void (async () => {
352
+ const payload = await fetchStatus()
353
+ if (payload?.ui?.showPanelStatus === false) return
354
+ statusFloater = buildStatusFloater()
355
+ })()
356
+
332
357
  return () => {
333
358
  if (refreshTimer !== undefined) window.clearTimeout(refreshTimer)
334
359
  window.clearInterval(syncInterval)
@@ -341,5 +366,6 @@ export function mountPanel(state: PanelState): () => void {
341
366
  unsubscribe()
342
367
  document.documentElement.removeAttribute(ACTIVE_ATTR)
343
368
  container?.remove()
369
+ statusFloater?.remove()
344
370
  }
345
371
  }
@@ -23,6 +23,7 @@ const STATE_ENDPOINT = '/dsh-tiddlywiki/admin/state'
23
23
  const INFO_ENDPOINT = '/dsh-tiddlywiki/admin/info'
24
24
  const CONFIG_ENDPOINT = '/dsh-tiddlywiki/admin/config'
25
25
  const RESTART_ENDPOINT = '/dsh-tiddlywiki/admin/restart'
26
+ const SYNC_ENDPOINT = '/dsh-tiddlywiki/sync'
26
27
 
27
28
  interface CatalogEntry {
28
29
  name: string
@@ -119,6 +120,34 @@ function renderStatus(row: HTMLElement, state: AdminState, refresh: () => Promis
119
120
  state.git?.dirty === true ? '有未提交改动' : '',
120
121
  ].filter(Boolean).join(' · ')
121
122
  const label = make('span', 'dsh-tw-settings-muted', info)
123
+ const sync = make('button', 'dsh-tw-settings-btn', '同步')
124
+ sync.type = 'button'
125
+ sync.title = 'git 同步(pull → commit → push)'
126
+ sync.addEventListener('click', () => {
127
+ sync.disabled = true
128
+ sync.textContent = '同步中…'
129
+ void (async () => {
130
+ try {
131
+ const res = await fetch(SYNC_ENDPOINT, { method: 'POST', signal: AbortSignal.timeout(120_000) })
132
+ const payload = (await res.json().catch(() => null)) as { ok?: boolean; message?: string; error?: string; changed?: boolean; restarted?: boolean; restartError?: string } | null
133
+ if (!res.ok || payload?.ok !== true) toast(`同步失败:${payload?.error ?? payload?.message ?? `HTTP ${res.status}`}`)
134
+ else {
135
+ let detail = ''
136
+ if (payload.changed === true) {
137
+ detail += payload.restarted === true ? ',TW 已重启' : ',TW 未自动重启'
138
+ if (payload.restartError) detail += `(${payload.restartError})`
139
+ }
140
+ toast(`同步完成:${payload.message ?? 'OK'}${detail}`)
141
+ }
142
+ } catch (err) {
143
+ toast(`同步失败:${err instanceof Error ? err.message : String(err)}`)
144
+ } finally {
145
+ sync.disabled = false
146
+ sync.textContent = '同步'
147
+ void refresh()
148
+ }
149
+ })()
150
+ })
122
151
  const restart = make('button', 'dsh-tw-settings-btn', '重启 TW')
123
152
  restart.type = 'button'
124
153
  restart.addEventListener('click', () => {
@@ -137,7 +166,7 @@ function renderStatus(row: HTMLElement, state: AdminState, refresh: () => Promis
137
166
  }
138
167
  })()
139
168
  })
140
- row.append(chip, label, restart)
169
+ row.append(chip, label, sync, restart)
141
170
  }
142
171
 
143
172
  /** Config section: fields bound to effective config, changed-only save. */
@@ -182,7 +211,8 @@ function renderConfigSection(body: HTMLElement, config: Record<string, unknown>,
182
211
  textField('git.remote', 'git 远端(空=仅本地)', typeof git.remote === 'string' ? git.remote : '')
183
212
  textField('git.branch', 'git 分支', typeof git.branch === 'string' ? git.branch : 'main')
184
213
  checkField('ui.showQuickNote', '显示「快速笔记」悬浮按钮', ui.showQuickNote !== false)
185
- checkField('ui.sidebarLeftCss', '侧边栏移左 CSS(随启动补丁还原被删除的样式)', ui.sidebarLeftCss !== false)
214
+ checkField('ui.showPanelStatus', '显示 TW 面板右下角「状态/重载」悬浮按钮', ui.showPanelStatus !== false)
215
+ checkField('ui.showSyncButton', '显示右下角「同步」悬浮按钮', ui.showSyncButton !== false)
186
216
  // 界面语言在下方「语言管理」区块设置(config 的 uiLanguage 仅供启动时自动应用)。
187
217
 
188
218
  const save = make('button', 'dsh-tw-settings-btn dsh-tw-settings-primary', '保存配置')
@@ -1,148 +1,148 @@
1
- /**
2
- * Sidebar entry injection — structure ported from dsh-taskboard's
3
- * sidebar-entry.ts (verified live in this shell): scope to the sidebar root,
4
- * find the New Session button, and insert the entry as a direct child of that
5
- * root next to the family block. A body-level MutationObserver self-heals
6
- * React re-renders; a slow timer covers shells that mount late without
7
- * further mutations. The row is plain DOM so it never disturbs the shell's
8
- * reconciliation.
9
- *
10
- * @module dsh-tiddlywiki/client/sidebar-entry
11
- */
12
- import type { PanelState } from './state.ts'
13
-
14
- /** Stable data attribute identifying this entry row. */
15
- export const ENTRY_SELECTOR = '[data-dsh-tw-entry]'
16
-
17
- /** Inline icon: a wiki page with a TiddlyWiki-style "T" (nav-icon look). */
18
- 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"><path d="M4 2.5h8a1 1 0 0 1 1 1v9a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1v-9a1 1 0 0 1 1-1z"/><path d="M6 6h4M6 8.5h2.5"/></svg>'
19
-
20
- /** Family entries from sibling plugins, kept in a stable relative order. */
21
- const FAMILY_SELECTOR = '[data-dsh-tw-entry], [data-dsh-atb-entry], [data-dsh-taskboard-entry], [data-dsh-ssh-entry]'
22
-
23
- /** Find the sidebar shell root element, or undefined while not yet mounted. */
24
- function sidebarRoot(): HTMLElement | undefined {
25
- const column = document.querySelector<HTMLElement>(
26
- '[data-pane="sidebar"], [class*="sidebarCol"], .dshDesktopUpstreamSidebar, .dshDesktopSidebarSurface',
27
- )
28
- if (column === null) return undefined
29
- const logoOwner = column.querySelector<HTMLElement>('[class*="logoRow"]')?.parentElement
30
- return logoOwner ?? (column.firstElementChild as HTMLElement | undefined)
31
- }
32
-
33
- /** The New Session button inside the sidebar root. */
34
- function newSessionButton(root: HTMLElement): HTMLButtonElement | undefined {
35
- const nested = root.querySelector<HTMLButtonElement>('button[class*="newSession"]')
36
- if (nested !== null) return nested
37
- for (const child of root.children) {
38
- if (child instanceof HTMLButtonElement && !child.matches(ENTRY_SELECTOR)) return child
39
- }
40
- const byAria = root.querySelector<HTMLButtonElement>(
41
- 'button[aria-label="新建会话"], button[aria-label="New Session"], button[aria-label*="新会话"], button[aria-label*="new session" i]',
42
- )
43
- if (byAria !== null) return byAria
44
- const buttons = Array.from(root.querySelectorAll<HTMLButtonElement>('button'))
45
- return buttons.find(button => !button.matches(ENTRY_SELECTOR) && /新会话|新建会话|new session/i.test(button.textContent ?? ''))
46
- }
47
-
48
- /** Build the entry row (a detached button; insert once the shell is up). */
49
- function createEntry(state: PanelState): HTMLButtonElement {
50
- const entry = document.createElement('button')
51
- entry.type = 'button'
52
- entry.dataset.dshTwEntry = ''
53
- entry.className = 'dsh-tw-entry'
54
- entry.setAttribute('aria-label', 'TiddlyWiki 知识库')
55
- entry.innerHTML = `<span class="dsh-tw-entry-icon">${ICON}</span><span class="dsh-tw-entry-label">TiddlyWiki</span>`
56
- entry.addEventListener('click', () => { state.toggle() })
57
- return entry
58
- }
59
-
60
- /** Re-insert the entry before the whole family block (stable ordering). */
61
- function placeEntry(root: HTMLElement, entry: HTMLButtonElement): boolean {
62
- const button = newSessionButton(root)
63
- if (button === undefined) return false
64
- if (entry.parentElement !== root) {
65
- const row = button.closest('[class*="logoRow"]')
66
- const base = (row !== null && row.parentElement === root) ? row : button
67
- const family = Array.from(root.children).filter(
68
- (el): el is HTMLElement => el instanceof HTMLElement && el.matches(FAMILY_SELECTOR),
69
- )
70
- const anchor = family.length > 0 ? (family[0] ?? null) : (base.nextElementSibling ?? null)
71
- root.insertBefore(entry, anchor)
72
- }
73
- return true
74
- }
75
-
76
- /** Debug counters (window.__twDebug) — evidence if the entry fails to appear. */
77
- interface TwDebug { attempts: number; found: boolean; placed: boolean }
78
-
79
- /**
80
- * Mount the sidebar entry, waiting for the shell and self-healing on later
81
- * re-renders.
82
- * @param state - the shared panel state the entry toggles.
83
- * @returns disposer removing the entry and its observers.
84
- */
85
- export function mountSidebarEntry(state: PanelState): () => void {
86
- const entry = createEntry(state)
87
- const debug: TwDebug = { attempts: 0, found: false, placed: false }
88
- const host = globalThis.location?.hostname
89
- if (host === 'localhost' || host === '127.0.0.1') {
90
- ;(window as unknown as { __twDebug?: TwDebug }).__twDebug = debug
91
- }
92
- let root: HTMLElement | undefined
93
- let placed = false
94
-
95
- const tryPlace = (): void => {
96
- debug.attempts++
97
- if (root !== undefined && !root.isConnected) {
98
- rootObserver.disconnect()
99
- root = undefined
100
- placed = false
101
- }
102
- if (placed) {
103
- if (document.body.contains(entry)) return
104
- rootObserver.disconnect()
105
- root = undefined
106
- placed = false
107
- }
108
- root ??= sidebarRoot()
109
- if (root === undefined) return
110
- debug.found = newSessionButton(root) !== undefined
111
- placed = placeEntry(root, entry)
112
- debug.placed = placed
113
- if (placed) rootObserver.observe(root, { childList: true, subtree: true })
114
- }
115
-
116
- // Body-level watcher as the whole-rebuild fallback.
117
- const waitObserver = new MutationObserver(() => { tryPlace() })
118
- waitObserver.observe(document.body, { childList: true, subtree: true })
119
-
120
- // Self-heal: re-insert in the same frame when a re-render displaces the row.
121
- const rootObserver = new MutationObserver(() => {
122
- if (root === undefined || !root.isConnected) {
123
- placed = false
124
- tryPlace()
125
- return
126
- }
127
- if (!root.contains(entry)) placed = placeEntry(root, entry)
128
- })
129
-
130
- // Belt-and-braces: a late shell mount with no further mutations still heals.
131
- const retry = setInterval(() => { tryPlace() }, 2_000)
132
-
133
- const syncActive = (): void => {
134
- if (state.isOpen()) entry.dataset.active = 'true'
135
- else delete entry.dataset.active
136
- }
137
- const unsubscribe = state.subscribe(syncActive)
138
- syncActive()
139
- tryPlace()
140
-
141
- return () => {
142
- clearInterval(retry)
143
- waitObserver.disconnect()
144
- rootObserver.disconnect()
145
- unsubscribe()
146
- entry.remove()
147
- }
148
- }
1
+ /**
2
+ * Sidebar entry injection — structure ported from dsh-taskboard's
3
+ * sidebar-entry.ts (verified live in this shell): scope to the sidebar root,
4
+ * find the New Session button, and insert the entry as a direct child of that
5
+ * root next to the family block. A body-level MutationObserver self-heals
6
+ * React re-renders; a slow timer covers shells that mount late without
7
+ * further mutations. The row is plain DOM so it never disturbs the shell's
8
+ * reconciliation.
9
+ *
10
+ * @module dsh-tiddlywiki/client/sidebar-entry
11
+ */
12
+ import type { PanelState } from './state.ts'
13
+
14
+ /** Stable data attribute identifying this entry row. */
15
+ export const ENTRY_SELECTOR = '[data-dsh-tw-entry]'
16
+
17
+ /** Inline icon: a wiki page with a TiddlyWiki-style "T" (nav-icon look). */
18
+ 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"><path d="M4 2.5h8a1 1 0 0 1 1 1v9a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1v-9a1 1 0 0 1 1-1z"/><path d="M6 6h4M6 8.5h2.5"/></svg>'
19
+
20
+ /** Family entries from sibling plugins, kept in a stable relative order. */
21
+ const FAMILY_SELECTOR = '[data-dsh-tw-entry], [data-dsh-atb-entry], [data-dsh-taskboard-entry], [data-dsh-ssh-entry]'
22
+
23
+ /** Find the sidebar shell root element, or undefined while not yet mounted. */
24
+ function sidebarRoot(): HTMLElement | undefined {
25
+ const column = document.querySelector<HTMLElement>(
26
+ '[data-pane="sidebar"], [class*="sidebarCol"], .dshDesktopUpstreamSidebar, .dshDesktopSidebarSurface',
27
+ )
28
+ if (column === null) return undefined
29
+ const logoOwner = column.querySelector<HTMLElement>('[class*="logoRow"]')?.parentElement
30
+ return logoOwner ?? (column.firstElementChild as HTMLElement | undefined)
31
+ }
32
+
33
+ /** The New Session button inside the sidebar root. */
34
+ function newSessionButton(root: HTMLElement): HTMLButtonElement | undefined {
35
+ const nested = root.querySelector<HTMLButtonElement>('button[class*="newSession"]')
36
+ if (nested !== null) return nested
37
+ for (const child of root.children) {
38
+ if (child instanceof HTMLButtonElement && !child.matches(ENTRY_SELECTOR)) return child
39
+ }
40
+ const byAria = root.querySelector<HTMLButtonElement>(
41
+ 'button[aria-label="新建会话"], button[aria-label="New Session"], button[aria-label*="新会话"], button[aria-label*="new session" i]',
42
+ )
43
+ if (byAria !== null) return byAria
44
+ const buttons = Array.from(root.querySelectorAll<HTMLButtonElement>('button'))
45
+ return buttons.find(button => !button.matches(ENTRY_SELECTOR) && /新会话|新建会话|new session/i.test(button.textContent ?? ''))
46
+ }
47
+
48
+ /** Build the entry row (a detached button; insert once the shell is up). */
49
+ function createEntry(state: PanelState): HTMLButtonElement {
50
+ const entry = document.createElement('button')
51
+ entry.type = 'button'
52
+ entry.dataset.dshTwEntry = ''
53
+ entry.className = 'dsh-tw-entry'
54
+ entry.setAttribute('aria-label', 'TiddlyWiki 知识库')
55
+ entry.innerHTML = `<span class="dsh-tw-entry-icon">${ICON}</span><span class="dsh-tw-entry-label">TiddlyWiki</span>`
56
+ entry.addEventListener('click', () => { state.toggle() })
57
+ return entry
58
+ }
59
+
60
+ /** Re-insert the entry before the whole family block (stable ordering). */
61
+ function placeEntry(root: HTMLElement, entry: HTMLButtonElement): boolean {
62
+ const button = newSessionButton(root)
63
+ if (button === undefined) return false
64
+ if (entry.parentElement !== root) {
65
+ const row = button.closest('[class*="logoRow"]')
66
+ const base = (row !== null && row.parentElement === root) ? row : button
67
+ const family = Array.from(root.children).filter(
68
+ (el): el is HTMLElement => el instanceof HTMLElement && el.matches(FAMILY_SELECTOR),
69
+ )
70
+ const anchor = family.length > 0 ? (family[0] ?? null) : (base.nextElementSibling ?? null)
71
+ root.insertBefore(entry, anchor)
72
+ }
73
+ return true
74
+ }
75
+
76
+ /** Debug counters (window.__twDebug) — evidence if the entry fails to appear. */
77
+ interface TwDebug { attempts: number; found: boolean; placed: boolean }
78
+
79
+ /**
80
+ * Mount the sidebar entry, waiting for the shell and self-healing on later
81
+ * re-renders.
82
+ * @param state - the shared panel state the entry toggles.
83
+ * @returns disposer removing the entry and its observers.
84
+ */
85
+ export function mountSidebarEntry(state: PanelState): () => void {
86
+ const entry = createEntry(state)
87
+ const debug: TwDebug = { attempts: 0, found: false, placed: false }
88
+ const host = globalThis.location?.hostname
89
+ if (host === 'localhost' || host === '127.0.0.1') {
90
+ ;(window as unknown as { __twDebug?: TwDebug }).__twDebug = debug
91
+ }
92
+ let root: HTMLElement | undefined
93
+ let placed = false
94
+
95
+ const tryPlace = (): void => {
96
+ debug.attempts++
97
+ if (root !== undefined && !root.isConnected) {
98
+ rootObserver.disconnect()
99
+ root = undefined
100
+ placed = false
101
+ }
102
+ if (placed) {
103
+ if (document.body.contains(entry)) return
104
+ rootObserver.disconnect()
105
+ root = undefined
106
+ placed = false
107
+ }
108
+ root ??= sidebarRoot()
109
+ if (root === undefined) return
110
+ debug.found = newSessionButton(root) !== undefined
111
+ placed = placeEntry(root, entry)
112
+ debug.placed = placed
113
+ if (placed) rootObserver.observe(root, { childList: true, subtree: true })
114
+ }
115
+
116
+ // Body-level watcher as the whole-rebuild fallback.
117
+ const waitObserver = new MutationObserver(() => { tryPlace() })
118
+ waitObserver.observe(document.body, { childList: true, subtree: true })
119
+
120
+ // Self-heal: re-insert in the same frame when a re-render displaces the row.
121
+ const rootObserver = new MutationObserver(() => {
122
+ if (root === undefined || !root.isConnected) {
123
+ placed = false
124
+ tryPlace()
125
+ return
126
+ }
127
+ if (!root.contains(entry)) placed = placeEntry(root, entry)
128
+ })
129
+
130
+ // Belt-and-braces: a late shell mount with no further mutations still heals.
131
+ const retry = setInterval(() => { tryPlace() }, 2_000)
132
+
133
+ const syncActive = (): void => {
134
+ if (state.isOpen()) entry.dataset.active = 'true'
135
+ else delete entry.dataset.active
136
+ }
137
+ const unsubscribe = state.subscribe(syncActive)
138
+ syncActive()
139
+ tryPlace()
140
+
141
+ return () => {
142
+ clearInterval(retry)
143
+ waitObserver.disconnect()
144
+ rootObserver.disconnect()
145
+ unsubscribe()
146
+ entry.remove()
147
+ }
148
+ }
@@ -1,39 +1,39 @@
1
- /**
2
- * Tiny pub/sub panel state shared by the sidebar entry and the center panel.
3
- *
4
- * @module dsh-tiddlywiki/client/state
5
- */
6
-
7
- export type PanelListener = (open: boolean) => void
8
-
9
- export class PanelState {
10
- private open = false
11
- private readonly listeners = new Set<PanelListener>()
12
-
13
- isOpen(): boolean {
14
- return this.open
15
- }
16
-
17
- toggle(): void {
18
- this.set(!this.open)
19
- }
20
-
21
- openPanel(): void {
22
- this.set(true)
23
- }
24
-
25
- closePanel(): void {
26
- this.set(false)
27
- }
28
-
29
- set(value: boolean): void {
30
- if (this.open === value) return
31
- this.open = value
32
- for (const listener of [...this.listeners]) listener(value)
33
- }
34
-
35
- subscribe(listener: PanelListener): () => void {
36
- this.listeners.add(listener)
37
- return () => { this.listeners.delete(listener) }
38
- }
39
- }
1
+ /**
2
+ * Tiny pub/sub panel state shared by the sidebar entry and the center panel.
3
+ *
4
+ * @module dsh-tiddlywiki/client/state
5
+ */
6
+
7
+ export type PanelListener = (open: boolean) => void
8
+
9
+ export class PanelState {
10
+ private open = false
11
+ private readonly listeners = new Set<PanelListener>()
12
+
13
+ isOpen(): boolean {
14
+ return this.open
15
+ }
16
+
17
+ toggle(): void {
18
+ this.set(!this.open)
19
+ }
20
+
21
+ openPanel(): void {
22
+ this.set(true)
23
+ }
24
+
25
+ closePanel(): void {
26
+ this.set(false)
27
+ }
28
+
29
+ set(value: boolean): void {
30
+ if (this.open === value) return
31
+ this.open = value
32
+ for (const listener of [...this.listeners]) listener(value)
33
+ }
34
+
35
+ subscribe(listener: PanelListener): () => void {
36
+ this.listeners.add(listener)
37
+ return () => { this.listeners.delete(listener) }
38
+ }
39
+ }