dsh-tiddlywiki 0.1.1 → 0.2.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 +206 -201
- package/cordis.patch.yml +13 -13
- package/lib/client.bundle.js +254 -19
- package/lib/client.js +254 -19
- package/lib/index.js +133 -22
- 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 +76 -76
- package/src/client/note-widget.ts +183 -20
- package/src/client/panel.ts +345 -304
- package/src/client/settings-page.ts +400 -397
- package/src/client/sidebar-entry.ts +148 -148
- package/src/client/state.ts +39 -39
- package/src/client/styles.ts +285 -235
- package/src/client/toast.ts +22 -22
- package/src/host/admin.ts +408 -408
- package/src/host/config.ts +92 -86
- package/src/host/git.ts +218 -218
- package/src/host/routes.ts +272 -233
- package/src/host/seed-notes.ts +122 -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 +346 -331
- package/src/sdk.ts +198 -198
|
@@ -3,9 +3,17 @@
|
|
|
3
3
|
* collapsible, independent of any shell DOM. Lets the human jot drafts /
|
|
4
4
|
* scratch notes while waiting for the AI or drafting the next prompt.
|
|
5
5
|
*
|
|
6
|
-
* Save posts to /dsh-tiddlywiki/note → an independent tiddler (title &
|
|
6
|
+
* Save posts to /dsh-tiddlywiki/note → an independent tiddler (title & tags
|
|
7
7
|
* editable; defaults: timestamp title + config tag, usually "inbox").
|
|
8
8
|
*
|
|
9
|
+
* ui.showQuickNote config: the widget is mounted async and stays hidden (and
|
|
10
|
+
* never appended) when the option is off, so the toggle button can be disabled
|
|
11
|
+
* from the settings page without touching the shell.
|
|
12
|
+
*
|
|
13
|
+
* Tag editor: multi-select chips + autocomplete from the wiki's existing tags
|
|
14
|
+
* (GET /dsh-tiddlywiki/tags). Enter/`,` commits the typed value; Backspace on
|
|
15
|
+
* an empty field removes the last chip; the dropdown filters on typing.
|
|
16
|
+
*
|
|
9
17
|
* @module dsh-tiddlywiki/client/note-widget
|
|
10
18
|
*/
|
|
11
19
|
import { toast } from './toast.ts'
|
|
@@ -14,6 +22,7 @@ import { openEditorPopup } from './editor-popup.ts'
|
|
|
14
22
|
const NOTE_ENDPOINT = '/dsh-tiddlywiki/note'
|
|
15
23
|
const EDIT_ENDPOINT = '/dsh-tiddlywiki/edit'
|
|
16
24
|
const STATUS_ENDPOINT = '/dsh-tiddlywiki/status'
|
|
25
|
+
const TAGS_ENDPOINT = '/dsh-tiddlywiki/tags'
|
|
17
26
|
|
|
18
27
|
function pad(n: number): string {
|
|
19
28
|
return n < 10 ? `0${n}` : String(n)
|
|
@@ -24,18 +33,157 @@ function timestampTitle(date = new Date()): string {
|
|
|
24
33
|
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}`
|
|
25
34
|
}
|
|
26
35
|
|
|
27
|
-
|
|
36
|
+
interface UiOptions {
|
|
37
|
+
showQuickNote: boolean
|
|
38
|
+
defaultTag: string
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function fetchUiOptions(): Promise<UiOptions> {
|
|
42
|
+
const fallback: UiOptions = { showQuickNote: true, defaultTag: 'inbox' }
|
|
28
43
|
try {
|
|
29
44
|
const res = await fetch(STATUS_ENDPOINT, { signal: AbortSignal.timeout(5_000) })
|
|
30
|
-
if (!res.ok) return
|
|
31
|
-
const payload = (await res.json()) as { note?: { tag?: string } }
|
|
32
|
-
return
|
|
45
|
+
if (!res.ok) return fallback
|
|
46
|
+
const payload = (await res.json()) as { ui?: { showQuickNote?: boolean }; note?: { tag?: string } }
|
|
47
|
+
return {
|
|
48
|
+
showQuickNote: payload.ui?.showQuickNote !== false,
|
|
49
|
+
defaultTag: payload.note?.tag ?? 'inbox',
|
|
50
|
+
}
|
|
33
51
|
} catch {
|
|
34
|
-
return
|
|
52
|
+
return fallback
|
|
35
53
|
}
|
|
36
54
|
}
|
|
37
55
|
|
|
38
|
-
|
|
56
|
+
/** Multi-tag chip editor with autocomplete from the wiki's existing tags. */
|
|
57
|
+
function buildTagEditor(): {
|
|
58
|
+
el: HTMLDivElement
|
|
59
|
+
getTags: () => string[]
|
|
60
|
+
setDefault: (tag: string) => void
|
|
61
|
+
} {
|
|
62
|
+
const wrap = document.createElement('div')
|
|
63
|
+
wrap.className = 'dsh-tw-note-tags'
|
|
64
|
+
|
|
65
|
+
const chipWrap = document.createElement('div')
|
|
66
|
+
chipWrap.className = 'dsh-tw-note-chips'
|
|
67
|
+
const input = document.createElement('input')
|
|
68
|
+
input.className = 'dsh-tw-note-taginput'
|
|
69
|
+
input.placeholder = 'tag(可多选,自动补全)'
|
|
70
|
+
const suggest = document.createElement('div')
|
|
71
|
+
suggest.className = 'dsh-tw-note-tagsuggest'
|
|
72
|
+
suggest.hidden = true
|
|
73
|
+
wrap.append(chipWrap, input, suggest)
|
|
74
|
+
|
|
75
|
+
const chips: string[] = []
|
|
76
|
+
const hideSuggest = (): void => { suggest.hidden = true }
|
|
77
|
+
|
|
78
|
+
const renderChips = (): void => {
|
|
79
|
+
chipWrap.replaceChildren()
|
|
80
|
+
for (const tag of chips) {
|
|
81
|
+
const chip = document.createElement('span')
|
|
82
|
+
chip.className = 'dsh-tw-note-tagchip'
|
|
83
|
+
chip.textContent = tag
|
|
84
|
+
const x = document.createElement('span')
|
|
85
|
+
x.className = 'dsh-tw-note-tagchip-x'
|
|
86
|
+
x.textContent = '×'
|
|
87
|
+
x.title = `移除 tag「${tag}」`
|
|
88
|
+
x.addEventListener('click', (event) => {
|
|
89
|
+
event.stopPropagation()
|
|
90
|
+
const i = chips.indexOf(tag)
|
|
91
|
+
if (i >= 0) {
|
|
92
|
+
chips.splice(i, 1)
|
|
93
|
+
renderChips()
|
|
94
|
+
}
|
|
95
|
+
})
|
|
96
|
+
chip.append(x)
|
|
97
|
+
chipWrap.append(chip)
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const addTag = (tag: string): void => {
|
|
102
|
+
const t = tag.trim()
|
|
103
|
+
if (t.length === 0 || chips.includes(t)) return
|
|
104
|
+
chips.push(t)
|
|
105
|
+
input.value = ''
|
|
106
|
+
renderChips()
|
|
107
|
+
hideSuggest()
|
|
108
|
+
input.focus()
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const commitInput = (): void => {
|
|
112
|
+
for (const raw of input.value.split(/\s+/)) addTag(raw)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Existing tags, fetched lazily once per widget lifetime.
|
|
116
|
+
let knownTags: string[] = []
|
|
117
|
+
let tagsPromise: Promise<string[]> | undefined
|
|
118
|
+
const ensureTags = (): Promise<string[]> => {
|
|
119
|
+
tagsPromise ??= fetch(TAGS_ENDPOINT, { signal: AbortSignal.timeout(5_000) })
|
|
120
|
+
.then((r) => (r.ok ? (r.json() as Promise<{ tags?: string[] }>) : Promise.resolve<{ tags?: string[] }>({})))
|
|
121
|
+
.then((p) => [...(p.tags ?? [])].sort((a, b) => a.localeCompare(b, 'zh')))
|
|
122
|
+
.catch(() => [])
|
|
123
|
+
void tagsPromise.then((list) => { knownTags = list })
|
|
124
|
+
return tagsPromise
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const showSuggest = (): void => {
|
|
128
|
+
const q = input.value.trim().toLowerCase()
|
|
129
|
+
const matches = knownTags
|
|
130
|
+
.filter((t) => !chips.includes(t) && (q.length === 0 || t.toLowerCase().includes(q)))
|
|
131
|
+
.slice(0, 8)
|
|
132
|
+
suggest.replaceChildren()
|
|
133
|
+
for (const tag of matches) {
|
|
134
|
+
const item = document.createElement('div')
|
|
135
|
+
item.className = 'dsh-tw-note-tagsuggest-item'
|
|
136
|
+
item.textContent = tag
|
|
137
|
+
item.addEventListener('mousedown', (event) => {
|
|
138
|
+
event.preventDefault()
|
|
139
|
+
addTag(tag)
|
|
140
|
+
})
|
|
141
|
+
suggest.append(item)
|
|
142
|
+
}
|
|
143
|
+
suggest.hidden = matches.length === 0
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
input.addEventListener('focus', () => { void ensureTags().then(showSuggest) })
|
|
147
|
+
input.addEventListener('input', () => {
|
|
148
|
+
if (knownTags.length === 0) void ensureTags().then(showSuggest)
|
|
149
|
+
else showSuggest()
|
|
150
|
+
})
|
|
151
|
+
input.addEventListener('keydown', (event) => {
|
|
152
|
+
if (event.key === 'Enter' || event.key === ',') {
|
|
153
|
+
event.preventDefault()
|
|
154
|
+
commitInput()
|
|
155
|
+
} else if (event.key === 'Backspace' && input.value.length === 0 && chips.length > 0) {
|
|
156
|
+
chips.pop()
|
|
157
|
+
renderChips()
|
|
158
|
+
} else if (event.key === 'Escape') {
|
|
159
|
+
hideSuggest()
|
|
160
|
+
}
|
|
161
|
+
})
|
|
162
|
+
document.addEventListener('click', (event) => {
|
|
163
|
+
if (!wrap.contains(event.target as Node)) hideSuggest()
|
|
164
|
+
}, true)
|
|
165
|
+
|
|
166
|
+
return {
|
|
167
|
+
el: wrap,
|
|
168
|
+
getTags: () => {
|
|
169
|
+
commitInput()
|
|
170
|
+
return [...chips]
|
|
171
|
+
},
|
|
172
|
+
setDefault: (tag: string) => {
|
|
173
|
+
// Only pre-fill when nothing is chosen yet; never steal focus.
|
|
174
|
+
if (chips.length === 0) {
|
|
175
|
+
const t = tag.trim()
|
|
176
|
+
if (t.length > 0) {
|
|
177
|
+
chips.push(t)
|
|
178
|
+
renderChips()
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
},
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** Build the whole widget DOM and wire it up (returns the appended root). */
|
|
186
|
+
function buildWidget(): HTMLDivElement {
|
|
39
187
|
const root = document.createElement('div')
|
|
40
188
|
root.className = 'dsh-tw-note'
|
|
41
189
|
|
|
@@ -60,10 +208,8 @@ export function mountNoteWidget(): () => void {
|
|
|
60
208
|
const titleInput = document.createElement('input')
|
|
61
209
|
titleInput.className = 'dsh-tw-note-title'
|
|
62
210
|
titleInput.placeholder = '标题(默认时间戳)'
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
tagInput.placeholder = 'tag(默认 inbox)'
|
|
66
|
-
fields.append(titleInput, tagInput)
|
|
211
|
+
const tagEditor = buildTagEditor()
|
|
212
|
+
fields.append(titleInput, tagEditor.el)
|
|
67
213
|
|
|
68
214
|
const textarea = document.createElement('textarea')
|
|
69
215
|
textarea.className = 'dsh-tw-note-text'
|
|
@@ -106,11 +252,10 @@ export function mountNoteWidget(): () => void {
|
|
|
106
252
|
card.hidden = false
|
|
107
253
|
opened = true
|
|
108
254
|
resetTitle()
|
|
109
|
-
if (tagInput.value.length === 0) tagInput.value = defaultTag
|
|
110
255
|
textarea.focus()
|
|
111
|
-
//
|
|
112
|
-
defaultTag = await
|
|
113
|
-
|
|
256
|
+
// Fetch the effective default tag once, then pre-fill if nothing is chosen.
|
|
257
|
+
defaultTag = (await fetchUiOptions()).defaultTag
|
|
258
|
+
tagEditor.setDefault(defaultTag)
|
|
114
259
|
}
|
|
115
260
|
|
|
116
261
|
const close = (): void => {
|
|
@@ -135,7 +280,7 @@ export function mountNoteWidget(): () => void {
|
|
|
135
280
|
headers: { 'content-type': 'application/json' },
|
|
136
281
|
body: JSON.stringify({
|
|
137
282
|
title: titleInput.value.trim(),
|
|
138
|
-
|
|
283
|
+
tags: tagEditor.getTags(),
|
|
139
284
|
text,
|
|
140
285
|
}),
|
|
141
286
|
signal: AbortSignal.timeout(10_000),
|
|
@@ -169,14 +314,14 @@ export function mountNoteWidget(): () => void {
|
|
|
169
314
|
const doEdit = async (): Promise<void> => {
|
|
170
315
|
const title = titleInput.value.trim().length > 0 ? titleInput.value.trim() : timestampTitle()
|
|
171
316
|
const text = textarea.value
|
|
172
|
-
const
|
|
317
|
+
const tags = tagEditor.getTags()
|
|
173
318
|
edit.disabled = true
|
|
174
319
|
edit.textContent = '打开中…'
|
|
175
320
|
try {
|
|
176
321
|
const res = await fetch(EDIT_ENDPOINT, {
|
|
177
322
|
method: 'POST',
|
|
178
323
|
headers: { 'content-type': 'application/json' },
|
|
179
|
-
body: JSON.stringify({ title,
|
|
324
|
+
body: JSON.stringify({ title, tags, text }),
|
|
180
325
|
signal: AbortSignal.timeout(10_000),
|
|
181
326
|
})
|
|
182
327
|
const payload = (await res.json().catch(() => null)) as
|
|
@@ -202,9 +347,27 @@ export function mountNoteWidget(): () => void {
|
|
|
202
347
|
|
|
203
348
|
edit.addEventListener('click', () => { void doEdit() })
|
|
204
349
|
|
|
205
|
-
return
|
|
206
|
-
|
|
350
|
+
return root
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Mount the floating quick-note widget. Fetches /status first: when
|
|
355
|
+
* `ui.showQuickNote` is off the widget is never created (no DOM side effects).
|
|
356
|
+
* Returns a disposer that removes it.
|
|
357
|
+
*/
|
|
358
|
+
export function mountNoteWidget(): () => void {
|
|
359
|
+
let disposed = false
|
|
360
|
+
let root: HTMLDivElement | undefined
|
|
361
|
+
const disposer = (): void => {
|
|
362
|
+
disposed = true
|
|
363
|
+
root?.remove()
|
|
207
364
|
const toastEl = document.querySelector<HTMLElement>('.dsh-tw-toast')
|
|
208
365
|
toastEl?.remove()
|
|
209
366
|
}
|
|
367
|
+
void (async () => {
|
|
368
|
+
const ui = await fetchUiOptions()
|
|
369
|
+
if (disposed || !ui.showQuickNote) return
|
|
370
|
+
root = buildWidget()
|
|
371
|
+
})()
|
|
372
|
+
return disposer
|
|
210
373
|
}
|