dsh-tiddlywiki 0.2.1 → 0.3.1
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 +542 -27
- package/lib/client.js +542 -27
- package/lib/index.js +232 -21
- package/lib/index.js.map +1 -1
- package/package.json +79 -79
- 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 +107 -11
- 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 +167 -9
- 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 +12 -6
- 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 {
|
|
@@ -95,9 +95,12 @@ html[data-dsh-tw-active] .dshDesktopConversationSurface > :not([data-dsh-tw-view
|
|
|
95
95
|
|
|
96
96
|
/* ── floating quick-note widget ────────────────────────────────
|
|
97
97
|
Positioned ABOVE the shutdown launcher FAB (fixed right:24 bottom:24,
|
|
98
|
-
z-index 900, 46px) and BELOW its confirm overlay (z-index 1000).
|
|
98
|
+
z-index 900, 46px) and BELOW its confirm overlay (z-index 1000).
|
|
99
|
+
Interactive overlays (the open card, the native-editor popup) sit ABOVE the
|
|
100
|
+
passive FAB column (sync/status, z-index 950) so those FABs never cover the
|
|
101
|
+
card's own bottom-right buttons. */
|
|
99
102
|
.dsh-tw-note {
|
|
100
|
-
position: fixed; right: 24px; bottom: 88px; z-index:
|
|
103
|
+
position: fixed; right: 24px; bottom: 88px; z-index: 980;
|
|
101
104
|
display: flex; flex-direction: column; align-items: flex-end; gap: 10px;
|
|
102
105
|
font-family: inherit;
|
|
103
106
|
}
|
|
@@ -108,11 +111,18 @@ html[data-dsh-tw-active] .dshDesktopConversationSurface > :not([data-dsh-tw-view
|
|
|
108
111
|
border-radius: 14px; box-shadow: var(--dsw-shadow-lv3, 0 8px 30px rgba(0,0,0,.22));
|
|
109
112
|
padding: 12px; display: flex; flex-direction: column; gap: 10px;
|
|
110
113
|
font-size: 13px;
|
|
114
|
+
backdrop-filter: blur(14px); -webkit-backdrop-filter: blur(14px);
|
|
115
|
+
animation: dsh-tw-note-in 180ms ease;
|
|
111
116
|
}
|
|
112
117
|
.dsh-tw-note-card[hidden] { display: none; }
|
|
118
|
+
@keyframes dsh-tw-note-in {
|
|
119
|
+
from { opacity: 0; transform: translateY(8px) scale(.98); }
|
|
120
|
+
to { opacity: 1; transform: translateY(0) scale(1); }
|
|
121
|
+
}
|
|
113
122
|
.dsh-tw-note-head {
|
|
114
123
|
display: flex; align-items: center; gap: 8px;
|
|
115
|
-
font-size: 12px; font-weight: 600;
|
|
124
|
+
font-size: 12px; font-weight: 600; letter-spacing: .2px;
|
|
125
|
+
color: var(--dsw-alias-label-primary, #222);
|
|
116
126
|
}
|
|
117
127
|
.dsh-tw-note-head .dsh-tw-note-label { margin-right: auto; display: flex; align-items: center; gap: 6px; }
|
|
118
128
|
.dsh-tw-note-close {
|
|
@@ -131,28 +141,91 @@ html[data-dsh-tw-active] .dshDesktopConversationSurface > :not([data-dsh-tw-view
|
|
|
131
141
|
outline: none; border-color: var(--dsw-alias-brand-primary, #3e63dd);
|
|
132
142
|
box-shadow: 0 0 0 2px color-mix(in srgb, var(--dsw-alias-brand-primary, #3e63dd) 25%, transparent);
|
|
133
143
|
}
|
|
134
|
-
.dsh-tw-note-text {
|
|
144
|
+
.dsh-tw-note-text::placeholder { color: var(--dsw-alias-label-dimmed, #999); }
|
|
145
|
+
/* ── Markdown editor (highlighted-textarea overlay) ────────────────
|
|
146
|
+
The <pre> highlight layer and the transparent <textarea> share identical
|
|
147
|
+
metrics (border/padding/font/line-height/white-space) so lines align 1:1. */
|
|
148
|
+
.dsh-tw-note-editor { position: relative; min-width: 0; }
|
|
149
|
+
.dsh-tw-note-editor pre,
|
|
150
|
+
.dsh-tw-note-editor textarea {
|
|
151
|
+
margin: 0; box-sizing: border-box; width: 100%;
|
|
135
152
|
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
|
-
|
|
153
|
+
padding: 8px 9px; font: inherit; font-size: 13px; line-height: 1.5; min-height: 120px;
|
|
154
|
+
white-space: pre-wrap; overflow-wrap: anywhere; word-break: break-word; tab-size: 4;
|
|
138
155
|
transition: border-color 120ms ease, box-shadow 120ms ease;
|
|
139
156
|
}
|
|
157
|
+
.dsh-tw-note-editor .dsh-tw-note-hl {
|
|
158
|
+
position: absolute; top: 0; left: 0; right: 0; bottom: 0; z-index: 0;
|
|
159
|
+
background: var(--dsw-alias-bg-layer-1, #fff); color: var(--dsw-alias-label-primary, #222);
|
|
160
|
+
pointer-events: none; overflow: hidden;
|
|
161
|
+
}
|
|
162
|
+
.dsh-tw-note-editor .dsh-tw-note-text {
|
|
163
|
+
position: relative; z-index: 1; resize: vertical;
|
|
164
|
+
background: transparent; color: transparent; caret-color: var(--dsw-alias-label-primary, #222);
|
|
165
|
+
overflow: auto;
|
|
166
|
+
}
|
|
167
|
+
.dsh-tw-note-editor .dsh-tw-note-text:focus { outline: none; }
|
|
168
|
+
.dsh-tw-note-drop .dsh-tw-note-text,
|
|
169
|
+
.dsh-tw-note-drop .dsh-tw-note-hl {
|
|
170
|
+
border-color: var(--dsw-alias-brand-primary, #3e63dd);
|
|
171
|
+
box-shadow: 0 0 0 2px color-mix(in srgb, var(--dsw-alias-brand-primary, #3e63dd) 25%, transparent);
|
|
172
|
+
}
|
|
173
|
+
/* ── markdown highlight tokens (colors only — never size/height, so the
|
|
174
|
+
overlay keeps line-for-line alignment with the invisible textarea) ── */
|
|
175
|
+
.dsh-tw-note-hl .md-heading { font-weight: 700; color: var(--dsw-alias-brand-primary, #3e63dd); }
|
|
176
|
+
.dsh-tw-note-hl .md-hash { font-weight: 700; color: color-mix(in srgb, var(--dsw-alias-brand-primary, #3e63dd) 55%, transparent); }
|
|
177
|
+
.dsh-tw-note-hl .md-code,
|
|
178
|
+
.dsh-tw-note-hl .md-code-block,
|
|
179
|
+
.dsh-tw-note-hl .md-fence {
|
|
180
|
+
font-family: ui-monospace, "Cascadia Mono", Consolas, "SF Mono", Menlo, monospace;
|
|
181
|
+
}
|
|
182
|
+
.dsh-tw-note-hl .md-code {
|
|
183
|
+
background: color-mix(in srgb, var(--dsw-alias-label-secondary, #888) 14%, transparent);
|
|
184
|
+
border-radius: 4px; padding: 0 3px;
|
|
185
|
+
}
|
|
186
|
+
.dsh-tw-note-hl .md-code-block,
|
|
187
|
+
.dsh-tw-note-hl .md-fence {
|
|
188
|
+
display: block; background: color-mix(in srgb, var(--dsw-alias-label-secondary, #888) 8%, transparent);
|
|
189
|
+
}
|
|
190
|
+
.dsh-tw-note-hl .md-bold { font-weight: 700; }
|
|
191
|
+
.dsh-tw-note-hl .md-italic { font-style: italic; }
|
|
192
|
+
.dsh-tw-note-hl .md-strike { text-decoration: line-through; opacity: .75; }
|
|
193
|
+
.dsh-tw-note-hl .md-link { color: var(--dsw-alias-brand-primary, #3e63dd); text-decoration: underline; }
|
|
194
|
+
.dsh-tw-note-hl .md-url { color: color-mix(in srgb, var(--dsw-alias-brand-primary, #3e63dd) 70%, #999); text-decoration: underline dotted; }
|
|
195
|
+
.dsh-tw-note-hl .md-image { color: var(--dsw-alias-state-success-primary, #3eaa5f); font-weight: 600; }
|
|
196
|
+
.dsh-tw-note-hl .md-bullet { font-weight: 700; color: var(--dsw-alias-brand-primary, #3e63dd); }
|
|
197
|
+
.dsh-tw-note-hl .md-number { font-weight: 700; color: var(--dsw-alias-state-warning-primary, #d9822b); }
|
|
198
|
+
.dsh-tw-note-hl .md-quote { font-style: italic; color: var(--dsw-alias-label-secondary, #888); }
|
|
199
|
+
.dsh-tw-note-hl .md-hr { color: var(--dsw-alias-label-dimmed, #999); text-decoration: line-through; }
|
|
200
|
+
.dsh-tw-note-hl .md-task { color: var(--dsw-alias-label-secondary, #888); }
|
|
201
|
+
.dsh-tw-note-hl .md-task-checked { color: var(--dsw-alias-state-success-primary, #3eaa5f); font-weight: 600; }
|
|
140
202
|
.dsh-tw-note-foot { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
|
|
203
|
+
.dsh-tw-note-foot-left, .dsh-tw-note-foot-right { display: flex; align-items: center; gap: 8px; }
|
|
141
204
|
.dsh-tw-note-hint { font-size: 11px; color: var(--dsw-alias-label-dimmed, #999); }
|
|
205
|
+
.dsh-tw-note-upload {
|
|
206
|
+
border: 1px solid var(--dsw-alias-border-l2, rgba(0,0,0,.18));
|
|
207
|
+
background: var(--dsw-alias-bg-layer-2, #fff); color: var(--dsw-alias-label-primary, #222);
|
|
208
|
+
font: inherit; font-size: 12px; padding: 6px 10px; border-radius: 8px; cursor: pointer;
|
|
209
|
+
transition: filter 120ms ease, transform 80ms ease;
|
|
210
|
+
}
|
|
211
|
+
.dsh-tw-note-upload:hover { filter: brightness(.96); }
|
|
212
|
+
.dsh-tw-note-upload:active { transform: scale(.96); }
|
|
142
213
|
.dsh-tw-note-save {
|
|
143
214
|
border: 1px solid transparent; background: var(--dsw-alias-brand-primary, #3e63dd); color: #fff;
|
|
144
215
|
font: inherit; font-size: 12px; padding: 6px 16px; border-radius: 8px; cursor: pointer;
|
|
145
|
-
transition: filter 120ms ease;
|
|
216
|
+
transition: filter 120ms ease, transform 80ms ease;
|
|
146
217
|
}
|
|
147
218
|
.dsh-tw-note-save:hover:not(:disabled) { filter: brightness(1.08); }
|
|
219
|
+
.dsh-tw-note-save:active:not(:disabled) { transform: scale(.96); }
|
|
148
220
|
.dsh-tw-note-save:disabled { opacity: .55; cursor: default; }
|
|
149
221
|
.dsh-tw-note-edit {
|
|
150
222
|
border: 1px solid var(--dsw-alias-border-l2, rgba(0,0,0,.18));
|
|
151
223
|
background: var(--dsw-alias-bg-layer-2, #fff); color: var(--dsw-alias-label-primary, #222);
|
|
152
224
|
font: inherit; font-size: 12px; padding: 6px 10px; border-radius: 8px; cursor: pointer;
|
|
153
|
-
transition: filter 120ms ease;
|
|
225
|
+
transition: filter 120ms ease, transform 80ms ease;
|
|
154
226
|
}
|
|
155
227
|
.dsh-tw-note-edit:hover:not(:disabled) { filter: brightness(.96); }
|
|
228
|
+
.dsh-tw-note-edit:active:not(:disabled) { transform: scale(.96); }
|
|
156
229
|
.dsh-tw-note-edit:disabled { opacity: .55; cursor: default; }
|
|
157
230
|
/* ── Multi-tag chip editor (quick-note tags) ─────────────────────────────── */
|
|
158
231
|
.dsh-tw-note-tags { position: relative; flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 4px; }
|
|
@@ -190,7 +263,7 @@ html[data-dsh-tw-active] .dshDesktopConversationSurface > :not([data-dsh-tw-view
|
|
|
190
263
|
background: var(--dsw-alias-bg-layer-2, #fff); color: var(--dsw-alias-label-primary, #222);
|
|
191
264
|
border: 1px solid var(--dsw-alias-border-l2, rgba(0,0,0,.2));
|
|
192
265
|
border-radius: 12px; box-shadow: 0 14px 44px rgba(0,0,0,.3);
|
|
193
|
-
z-index:
|
|
266
|
+
z-index: 990; font-family: inherit; font-size: 13px;
|
|
194
267
|
}
|
|
195
268
|
.dsh-tw-editor-bar {
|
|
196
269
|
display: flex; align-items: center; gap: 8px; flex: 0 0 auto;
|
|
@@ -216,6 +289,29 @@ html[data-dsh-tw-active] .dshDesktopConversationSurface > :not([data-dsh-tw-view
|
|
|
216
289
|
}
|
|
217
290
|
.dsh-tw-note-toggle:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,.1)); }
|
|
218
291
|
|
|
292
|
+
/* ── floating sync button (bottom-right) ───────────────────────
|
|
293
|
+
Pill FAB between the quick-note toggle (bottom:88) and the TW panel
|
|
294
|
+
status/reload floaters (bottom:192); dot reflects git state. */
|
|
295
|
+
.dsh-tw-sync {
|
|
296
|
+
position: fixed; right: 24px; bottom: 140px; z-index: 950;
|
|
297
|
+
display: inline-flex; align-items: center; gap: 7px;
|
|
298
|
+
padding: 9px 14px; border-radius: 999px;
|
|
299
|
+
border: 1px solid var(--dsw-alias-border-l2, rgba(0,0,0,.18));
|
|
300
|
+
background: var(--dsw-alias-bg-layer-2, #fff); color: var(--dsw-alias-label-primary, #222);
|
|
301
|
+
font: inherit; font-size: 13px; box-shadow: var(--dsw-shadow-lv3, 0 4px 16px rgba(0,0,0,.16));
|
|
302
|
+
cursor: pointer; transition: background-color 120ms ease;
|
|
303
|
+
}
|
|
304
|
+
.dsh-tw-sync:hover:not(:disabled) { background: var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,.1)); }
|
|
305
|
+
.dsh-tw-sync:disabled { opacity: .65; cursor: default; }
|
|
306
|
+
.dsh-tw-sync-icon { display: inline-flex; flex: none; }
|
|
307
|
+
.dsh-tw-sync-spin .dsh-tw-sync-icon svg { animation: dsh-tw-spin 1s linear infinite; transform-origin: center; }
|
|
308
|
+
@keyframes dsh-tw-spin { to { transform: rotate(360deg); } }
|
|
309
|
+
.dsh-tw-sync-dot { width: 8px; height: 8px; border-radius: 50%; background: #999; flex: none; }
|
|
310
|
+
.dsh-tw-sync-dot[data-state="clean"] { background: var(--dsw-alias-state-success-primary, #3eaa5f); }
|
|
311
|
+
.dsh-tw-sync-dot[data-state="dirty"] { background: var(--dsw-alias-state-warning-primary, #d9822b); }
|
|
312
|
+
.dsh-tw-sync-dot[data-state="behind"] { background: var(--dsw-alias-state-error-primary, #d13b3b); }
|
|
313
|
+
.dsh-tw-sync-dot[data-state="syncing"] { background: var(--dsw-alias-brand-primary, #3e63dd); }
|
|
314
|
+
|
|
219
315
|
/* ── toast ──────────────────────────────────────────────────── */
|
|
220
316
|
.dsh-tw-toast {
|
|
221
317
|
position: fixed; left: 50%; bottom: 96px; transform: translateX(-50%);
|