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.
- package/LICENSE +21 -21
- package/README.md +203 -172
- package/cordis.patch.yml +13 -13
- package/lib/client.bundle.js +46 -1
- package/lib/client.js +46 -1
- package/lib/index.js +2152 -2152
- package/package.json +79 -63
- package/src/client/editor-popup.ts +121 -121
- package/src/client/index.ts +76 -76
- package/src/client/note-widget.ts +210 -210
- package/src/client/panel.ts +345 -304
- package/src/client/settings-page.ts +397 -397
- package/src/client/sidebar-entry.ts +148 -148
- package/src/client/state.ts +39 -39
- package/src/client/styles.ts +242 -235
- package/src/client/toast.ts +22 -22
- package/src/host/admin.ts +408 -408
- package/src/host/config.ts +86 -86
- package/src/host/git.ts +218 -218
- package/src/host/routes.ts +233 -233
- package/src/host/seed-notes.ts +62 -62
- package/src/host/tools.ts +254 -254
- package/src/host/tw-api.ts +157 -157
- package/src/host/wiki.ts +287 -287
- package/src/index.ts +331 -331
- package/src/sdk.ts +198 -198
|
@@ -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
|
+
}
|
package/src/client/state.ts
CHANGED
|
@@ -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
|
+
}
|