dsh-tiddlywiki 0.1.0 → 0.1.2

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,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
+ }