dsh-tiddlywiki 0.2.1 → 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.
- package/LICENSE +21 -21
- package/README.md +10 -4
- package/cordis.patch.yml +13 -13
- package/lib/client.bundle.js +2173 -1671
- package/lib/client.js +2183 -1681
- package/lib/index.js +2424 -2225
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
- package/src/client/editor-popup.ts +121 -121
- package/src/client/index.ts +2 -0
- package/src/client/markdown.ts +137 -0
- package/src/client/note-widget.ts +515 -373
- package/src/client/settings-page.ts +31 -1
- package/src/client/sidebar-entry.ts +148 -148
- package/src/client/state.ts +39 -39
- package/src/client/styles.ts +88 -5
- package/src/client/sync-button.ts +188 -0
- package/src/client/toast.ts +22 -22
- package/src/host/admin.ts +408 -408
- package/src/host/config.ts +2 -0
- package/src/host/git.ts +13 -3
- package/src/host/routes.ts +151 -1
- package/src/host/tools.ts +28 -1
- package/src/host/tw-api.ts +157 -157
- package/src/host/wiki.ts +287 -287
- package/src/index.ts +9 -5
- package/src/sdk.ts +198 -198
|
@@ -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. */
|
|
@@ -183,6 +212,7 @@ function renderConfigSection(body: HTMLElement, config: Record<string, unknown>,
|
|
|
183
212
|
textField('git.branch', 'git 分支', typeof git.branch === 'string' ? git.branch : 'main')
|
|
184
213
|
checkField('ui.showQuickNote', '显示「快速笔记」悬浮按钮', ui.showQuickNote !== false)
|
|
185
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
|
+
}
|
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
|
+
}
|
package/src/client/styles.ts
CHANGED
|
@@ -58,9 +58,9 @@ html[data-dsh-tw-active] .dshDesktopConversationSurface > :not([data-dsh-tw-view
|
|
|
58
58
|
setting controls whether they mount at all. */
|
|
59
59
|
.dsh-tw-panel-status {
|
|
60
60
|
/* 状态/重载悬浮按钮:右对齐到与快速笔记按钮同一条竖线(right:24),
|
|
61
|
-
|
|
61
|
+
垂直排在「同步」按钮(bottom:140)上方;
|
|
62
62
|
两个按钮竖直依次排列(状态在上、重载在下),与其他插件按钮列对齐、不堆叠。 */
|
|
63
|
-
position: fixed; right: 24px; bottom:
|
|
63
|
+
position: fixed; right: 24px; bottom: 192px; z-index: 950;
|
|
64
64
|
display: flex; flex-direction: column; align-items: flex-end; gap: 8px;
|
|
65
65
|
}
|
|
66
66
|
.dsh-tw-panel-status-btn {
|
|
@@ -131,14 +131,74 @@ html[data-dsh-tw-active] .dshDesktopConversationSurface > :not([data-dsh-tw-view
|
|
|
131
131
|
outline: none; border-color: var(--dsw-alias-brand-primary, #3e63dd);
|
|
132
132
|
box-shadow: 0 0 0 2px color-mix(in srgb, var(--dsw-alias-brand-primary, #3e63dd) 25%, transparent);
|
|
133
133
|
}
|
|
134
|
-
.dsh-tw-note-text {
|
|
134
|
+
.dsh-tw-note-text::placeholder { color: var(--dsw-alias-label-dimmed, #999); }
|
|
135
|
+
/* ── Markdown editor (highlighted-textarea overlay) ────────────────
|
|
136
|
+
The <pre> highlight layer and the transparent <textarea> share identical
|
|
137
|
+
metrics (border/padding/font/line-height/white-space) so lines align 1:1. */
|
|
138
|
+
.dsh-tw-note-editor { position: relative; min-width: 0; }
|
|
139
|
+
.dsh-tw-note-editor pre,
|
|
140
|
+
.dsh-tw-note-editor textarea {
|
|
141
|
+
margin: 0; box-sizing: border-box; width: 100%;
|
|
135
142
|
border: 1px solid var(--dsw-alias-border-l2, rgba(0,0,0,.18)); border-radius: 8px;
|
|
136
|
-
padding: 8px 9px; font: inherit; font-size: 13px;
|
|
137
|
-
|
|
143
|
+
padding: 8px 9px; font: inherit; font-size: 13px; line-height: 1.5; min-height: 120px;
|
|
144
|
+
white-space: pre-wrap; overflow-wrap: anywhere; word-break: break-word; tab-size: 4;
|
|
138
145
|
transition: border-color 120ms ease, box-shadow 120ms ease;
|
|
139
146
|
}
|
|
147
|
+
.dsh-tw-note-editor .dsh-tw-note-hl {
|
|
148
|
+
position: absolute; top: 0; left: 0; right: 0; bottom: 0; z-index: 0;
|
|
149
|
+
background: var(--dsw-alias-bg-layer-1, #fff); color: var(--dsw-alias-label-primary, #222);
|
|
150
|
+
pointer-events: none; overflow: hidden;
|
|
151
|
+
}
|
|
152
|
+
.dsh-tw-note-editor .dsh-tw-note-text {
|
|
153
|
+
position: relative; z-index: 1; resize: vertical;
|
|
154
|
+
background: transparent; color: transparent; caret-color: var(--dsw-alias-label-primary, #222);
|
|
155
|
+
overflow: auto;
|
|
156
|
+
}
|
|
157
|
+
.dsh-tw-note-editor .dsh-tw-note-text:focus { outline: none; }
|
|
158
|
+
.dsh-tw-note-drop .dsh-tw-note-text,
|
|
159
|
+
.dsh-tw-note-drop .dsh-tw-note-hl {
|
|
160
|
+
border-color: var(--dsw-alias-brand-primary, #3e63dd);
|
|
161
|
+
box-shadow: 0 0 0 2px color-mix(in srgb, var(--dsw-alias-brand-primary, #3e63dd) 25%, transparent);
|
|
162
|
+
}
|
|
163
|
+
/* ── markdown highlight tokens (colors only — never size/height, so the
|
|
164
|
+
overlay keeps line-for-line alignment with the invisible textarea) ── */
|
|
165
|
+
.dsh-tw-note-hl .md-heading { font-weight: 700; color: var(--dsw-alias-brand-primary, #3e63dd); }
|
|
166
|
+
.dsh-tw-note-hl .md-hash { font-weight: 700; color: color-mix(in srgb, var(--dsw-alias-brand-primary, #3e63dd) 55%, transparent); }
|
|
167
|
+
.dsh-tw-note-hl .md-code,
|
|
168
|
+
.dsh-tw-note-hl .md-code-block,
|
|
169
|
+
.dsh-tw-note-hl .md-fence {
|
|
170
|
+
font-family: ui-monospace, "Cascadia Mono", Consolas, "SF Mono", Menlo, monospace;
|
|
171
|
+
}
|
|
172
|
+
.dsh-tw-note-hl .md-code {
|
|
173
|
+
background: color-mix(in srgb, var(--dsw-alias-label-secondary, #888) 14%, transparent);
|
|
174
|
+
border-radius: 4px; padding: 0 3px;
|
|
175
|
+
}
|
|
176
|
+
.dsh-tw-note-hl .md-code-block,
|
|
177
|
+
.dsh-tw-note-hl .md-fence {
|
|
178
|
+
display: block; background: color-mix(in srgb, var(--dsw-alias-label-secondary, #888) 8%, transparent);
|
|
179
|
+
}
|
|
180
|
+
.dsh-tw-note-hl .md-bold { font-weight: 700; }
|
|
181
|
+
.dsh-tw-note-hl .md-italic { font-style: italic; }
|
|
182
|
+
.dsh-tw-note-hl .md-strike { text-decoration: line-through; opacity: .75; }
|
|
183
|
+
.dsh-tw-note-hl .md-link { color: var(--dsw-alias-brand-primary, #3e63dd); text-decoration: underline; }
|
|
184
|
+
.dsh-tw-note-hl .md-url { color: color-mix(in srgb, var(--dsw-alias-brand-primary, #3e63dd) 70%, #999); text-decoration: underline dotted; }
|
|
185
|
+
.dsh-tw-note-hl .md-image { color: var(--dsw-alias-state-success-primary, #3eaa5f); font-weight: 600; }
|
|
186
|
+
.dsh-tw-note-hl .md-bullet { font-weight: 700; color: var(--dsw-alias-brand-primary, #3e63dd); }
|
|
187
|
+
.dsh-tw-note-hl .md-number { font-weight: 700; color: var(--dsw-alias-state-warning-primary, #d9822b); }
|
|
188
|
+
.dsh-tw-note-hl .md-quote { font-style: italic; color: var(--dsw-alias-label-secondary, #888); }
|
|
189
|
+
.dsh-tw-note-hl .md-hr { color: var(--dsw-alias-label-dimmed, #999); text-decoration: line-through; }
|
|
190
|
+
.dsh-tw-note-hl .md-task { color: var(--dsw-alias-label-secondary, #888); }
|
|
191
|
+
.dsh-tw-note-hl .md-task-checked { color: var(--dsw-alias-state-success-primary, #3eaa5f); font-weight: 600; }
|
|
140
192
|
.dsh-tw-note-foot { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
|
|
193
|
+
.dsh-tw-note-foot-left, .dsh-tw-note-foot-right { display: flex; align-items: center; gap: 8px; }
|
|
141
194
|
.dsh-tw-note-hint { font-size: 11px; color: var(--dsw-alias-label-dimmed, #999); }
|
|
195
|
+
.dsh-tw-note-upload {
|
|
196
|
+
border: 1px solid var(--dsw-alias-border-l2, rgba(0,0,0,.18));
|
|
197
|
+
background: var(--dsw-alias-bg-layer-2, #fff); color: var(--dsw-alias-label-primary, #222);
|
|
198
|
+
font: inherit; font-size: 12px; padding: 6px 10px; border-radius: 8px; cursor: pointer;
|
|
199
|
+
transition: filter 120ms ease;
|
|
200
|
+
}
|
|
201
|
+
.dsh-tw-note-upload:hover { filter: brightness(.96); }
|
|
142
202
|
.dsh-tw-note-save {
|
|
143
203
|
border: 1px solid transparent; background: var(--dsw-alias-brand-primary, #3e63dd); color: #fff;
|
|
144
204
|
font: inherit; font-size: 12px; padding: 6px 16px; border-radius: 8px; cursor: pointer;
|
|
@@ -216,6 +276,29 @@ html[data-dsh-tw-active] .dshDesktopConversationSurface > :not([data-dsh-tw-view
|
|
|
216
276
|
}
|
|
217
277
|
.dsh-tw-note-toggle:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,.1)); }
|
|
218
278
|
|
|
279
|
+
/* ── floating sync button (bottom-right) ───────────────────────
|
|
280
|
+
Pill FAB between the quick-note toggle (bottom:88) and the TW panel
|
|
281
|
+
status/reload floaters (bottom:192); dot reflects git state. */
|
|
282
|
+
.dsh-tw-sync {
|
|
283
|
+
position: fixed; right: 24px; bottom: 140px; z-index: 950;
|
|
284
|
+
display: inline-flex; align-items: center; gap: 7px;
|
|
285
|
+
padding: 9px 14px; border-radius: 999px;
|
|
286
|
+
border: 1px solid var(--dsw-alias-border-l2, rgba(0,0,0,.18));
|
|
287
|
+
background: var(--dsw-alias-bg-layer-2, #fff); color: var(--dsw-alias-label-primary, #222);
|
|
288
|
+
font: inherit; font-size: 13px; box-shadow: var(--dsw-shadow-lv3, 0 4px 16px rgba(0,0,0,.16));
|
|
289
|
+
cursor: pointer; transition: background-color 120ms ease;
|
|
290
|
+
}
|
|
291
|
+
.dsh-tw-sync:hover:not(:disabled) { background: var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,.1)); }
|
|
292
|
+
.dsh-tw-sync:disabled { opacity: .65; cursor: default; }
|
|
293
|
+
.dsh-tw-sync-icon { display: inline-flex; flex: none; }
|
|
294
|
+
.dsh-tw-sync-spin .dsh-tw-sync-icon svg { animation: dsh-tw-spin 1s linear infinite; transform-origin: center; }
|
|
295
|
+
@keyframes dsh-tw-spin { to { transform: rotate(360deg); } }
|
|
296
|
+
.dsh-tw-sync-dot { width: 8px; height: 8px; border-radius: 50%; background: #999; flex: none; }
|
|
297
|
+
.dsh-tw-sync-dot[data-state="clean"] { background: var(--dsw-alias-state-success-primary, #3eaa5f); }
|
|
298
|
+
.dsh-tw-sync-dot[data-state="dirty"] { background: var(--dsw-alias-state-warning-primary, #d9822b); }
|
|
299
|
+
.dsh-tw-sync-dot[data-state="behind"] { background: var(--dsw-alias-state-error-primary, #d13b3b); }
|
|
300
|
+
.dsh-tw-sync-dot[data-state="syncing"] { background: var(--dsw-alias-brand-primary, #3e63dd); }
|
|
301
|
+
|
|
219
302
|
/* ── toast ──────────────────────────────────────────────────── */
|
|
220
303
|
.dsh-tw-toast {
|
|
221
304
|
position: fixed; left: 50%; bottom: 96px; transform: translateX(-50%);
|