dsh-tiddlywiki 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,210 +1,210 @@
1
- /**
2
- * Floating quick-note widget (design doc §12, D6/D7) — bottom-right, fixed,
3
- * collapsible, independent of any shell DOM. Lets the human jot drafts /
4
- * scratch notes while waiting for the AI or drafting the next prompt.
5
- *
6
- * Save posts to /dsh-tiddlywiki/note → an independent tiddler (title & tag
7
- * editable; defaults: timestamp title + config tag, usually "inbox").
8
- *
9
- * @module dsh-tiddlywiki/client/note-widget
10
- */
11
- import { toast } from './toast.ts'
12
- import { openEditorPopup } from './editor-popup.ts'
13
-
14
- const NOTE_ENDPOINT = '/dsh-tiddlywiki/note'
15
- const EDIT_ENDPOINT = '/dsh-tiddlywiki/edit'
16
- const STATUS_ENDPOINT = '/dsh-tiddlywiki/status'
17
-
18
- function pad(n: number): string {
19
- return n < 10 ? `0${n}` : String(n)
20
- }
21
-
22
- /** Default note title: `YYYY-MM-DD HH:mm`. */
23
- function timestampTitle(date = new Date()): string {
24
- return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}`
25
- }
26
-
27
- async function fetchDefaultTag(): Promise<string> {
28
- try {
29
- const res = await fetch(STATUS_ENDPOINT, { signal: AbortSignal.timeout(5_000) })
30
- if (!res.ok) return 'inbox'
31
- const payload = (await res.json()) as { note?: { tag?: string } }
32
- return payload.note?.tag ?? 'inbox'
33
- } catch {
34
- return 'inbox'
35
- }
36
- }
37
-
38
- export function mountNoteWidget(): () => void {
39
- const root = document.createElement('div')
40
- root.className = 'dsh-tw-note'
41
-
42
- const card = document.createElement('div')
43
- card.className = 'dsh-tw-note-card'
44
- card.hidden = true
45
-
46
- const head = document.createElement('div')
47
- head.className = 'dsh-tw-note-head'
48
- const label = document.createElement('span')
49
- label.className = 'dsh-tw-note-label'
50
- label.textContent = '📝 快速笔记'
51
- const closeBtn = document.createElement('button')
52
- closeBtn.type = 'button'
53
- closeBtn.className = 'dsh-tw-note-close'
54
- closeBtn.title = '收起'
55
- closeBtn.textContent = '✕'
56
- head.append(label, closeBtn)
57
-
58
- const fields = document.createElement('div')
59
- fields.className = 'dsh-tw-note-fields'
60
- const titleInput = document.createElement('input')
61
- titleInput.className = 'dsh-tw-note-title'
62
- titleInput.placeholder = '标题(默认时间戳)'
63
- const tagInput = document.createElement('input')
64
- tagInput.className = 'dsh-tw-note-tag'
65
- tagInput.placeholder = 'tag(默认 inbox)'
66
- fields.append(titleInput, tagInput)
67
-
68
- const textarea = document.createElement('textarea')
69
- textarea.className = 'dsh-tw-note-text'
70
- textarea.placeholder = '写点东西…(Ctrl+Enter 保存)'
71
-
72
- const foot = document.createElement('div')
73
- foot.className = 'dsh-tw-note-foot'
74
- const hint = document.createElement('span')
75
- hint.className = 'dsh-tw-note-hint'
76
- hint.textContent = 'Ctrl+Enter'
77
- const edit = document.createElement('button')
78
- edit.type = 'button'
79
- edit.className = 'dsh-tw-note-edit'
80
- edit.title = '保存并在 TiddlyWiki 原生编辑器中打开'
81
- edit.textContent = '✏️ 在 TW 中编辑'
82
- const save = document.createElement('button')
83
- save.type = 'button'
84
- save.className = 'dsh-tw-note-save'
85
- save.textContent = '保存'
86
- foot.append(hint, edit, save)
87
-
88
- card.append(head, fields, textarea, foot)
89
-
90
- const toggle = document.createElement('button')
91
- toggle.type = 'button'
92
- toggle.className = 'dsh-tw-note-toggle'
93
- toggle.textContent = '📝 快速笔记'
94
-
95
- root.append(card, toggle)
96
- document.body.append(root)
97
-
98
- let defaultTag = 'inbox'
99
- let opened = false
100
-
101
- const resetTitle = (): void => {
102
- titleInput.value = timestampTitle()
103
- }
104
-
105
- const open = async (): Promise<void> => {
106
- card.hidden = false
107
- opened = true
108
- resetTitle()
109
- if (tagInput.value.length === 0) tagInput.value = defaultTag
110
- textarea.focus()
111
- // Refresh the configured default tag on each open (cheap, best effort).
112
- defaultTag = await fetchDefaultTag()
113
- if (tagInput.value.length === 0) tagInput.value = defaultTag
114
- }
115
-
116
- const close = (): void => {
117
- card.hidden = true
118
- opened = false
119
- }
120
-
121
- toggle.addEventListener('click', () => { void (opened ? close() : open()) })
122
- closeBtn.addEventListener('click', close)
123
-
124
- const doSave = async (): Promise<void> => {
125
- const text = textarea.value.trim()
126
- if (text.length === 0) {
127
- toast('内容为空,未保存')
128
- return
129
- }
130
- save.disabled = true
131
- save.textContent = '保存中…'
132
- try {
133
- const res = await fetch(NOTE_ENDPOINT, {
134
- method: 'POST',
135
- headers: { 'content-type': 'application/json' },
136
- body: JSON.stringify({
137
- title: titleInput.value.trim(),
138
- tag: tagInput.value.trim(),
139
- text,
140
- }),
141
- signal: AbortSignal.timeout(10_000),
142
- })
143
- const payload = (await res.json().catch(() => null)) as { ok?: boolean; title?: string; error?: string } | null
144
- if (!res.ok || payload?.ok !== true) {
145
- toast(`保存失败:${payload?.error ?? `HTTP ${res.status}`}`)
146
- return
147
- }
148
- textarea.value = ''
149
- resetTitle()
150
- toast(`已保存「${payload.title ?? titleInput.value}」`)
151
- close()
152
- } catch (err) {
153
- toast(`保存失败:${err instanceof Error ? err.message : String(err)}`)
154
- } finally {
155
- save.disabled = false
156
- save.textContent = '保存'
157
- }
158
- }
159
-
160
- save.addEventListener('click', () => { void doSave() })
161
- textarea.addEventListener('keydown', (event) => {
162
- if ((event.ctrlKey || event.metaKey) && event.key === 'Enter') {
163
- event.preventDefault()
164
- void doSave()
165
- }
166
- })
167
-
168
- /** Save (if non-empty) and open the tiddler in TW's native editor. */
169
- const doEdit = async (): Promise<void> => {
170
- const title = titleInput.value.trim().length > 0 ? titleInput.value.trim() : timestampTitle()
171
- const text = textarea.value
172
- const tag = tagInput.value.trim()
173
- edit.disabled = true
174
- edit.textContent = '打开中…'
175
- try {
176
- const res = await fetch(EDIT_ENDPOINT, {
177
- method: 'POST',
178
- headers: { 'content-type': 'application/json' },
179
- body: JSON.stringify({ title, tag, text }),
180
- signal: AbortSignal.timeout(10_000),
181
- })
182
- const payload = (await res.json().catch(() => null)) as
183
- | { ok?: boolean; title?: string; draftTitle?: string; twUrl?: string; error?: string }
184
- | null
185
- if (!res.ok || payload?.ok !== true) {
186
- toast(`打开失败:${payload?.error ?? `HTTP ${res.status}`}`)
187
- return
188
- }
189
- if (typeof payload.twUrl !== 'string' || typeof payload.draftTitle !== 'string') {
190
- toast('打开失败:服务未返回编辑器地址')
191
- return
192
- }
193
- openEditorPopup(`${payload.twUrl}#${encodeURIComponent(payload.draftTitle)}`, payload.title ?? title)
194
- toast(`已在弹出窗口打开「${payload.title ?? title}」编辑器`)
195
- } catch (err) {
196
- toast(`打开失败:${err instanceof Error ? err.message : String(err)}`)
197
- } finally {
198
- edit.disabled = false
199
- edit.textContent = '✏️ 在 TW 中编辑'
200
- }
201
- }
202
-
203
- edit.addEventListener('click', () => { void doEdit() })
204
-
205
- return () => {
206
- root.remove()
207
- const toastEl = document.querySelector<HTMLElement>('.dsh-tw-toast')
208
- toastEl?.remove()
209
- }
210
- }
1
+ /**
2
+ * Floating quick-note widget (design doc §12, D6/D7) — bottom-right, fixed,
3
+ * collapsible, independent of any shell DOM. Lets the human jot drafts /
4
+ * scratch notes while waiting for the AI or drafting the next prompt.
5
+ *
6
+ * Save posts to /dsh-tiddlywiki/note → an independent tiddler (title & tag
7
+ * editable; defaults: timestamp title + config tag, usually "inbox").
8
+ *
9
+ * @module dsh-tiddlywiki/client/note-widget
10
+ */
11
+ import { toast } from './toast.ts'
12
+ import { openEditorPopup } from './editor-popup.ts'
13
+
14
+ const NOTE_ENDPOINT = '/dsh-tiddlywiki/note'
15
+ const EDIT_ENDPOINT = '/dsh-tiddlywiki/edit'
16
+ const STATUS_ENDPOINT = '/dsh-tiddlywiki/status'
17
+
18
+ function pad(n: number): string {
19
+ return n < 10 ? `0${n}` : String(n)
20
+ }
21
+
22
+ /** Default note title: `YYYY-MM-DD HH:mm`. */
23
+ function timestampTitle(date = new Date()): string {
24
+ return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}`
25
+ }
26
+
27
+ async function fetchDefaultTag(): Promise<string> {
28
+ try {
29
+ const res = await fetch(STATUS_ENDPOINT, { signal: AbortSignal.timeout(5_000) })
30
+ if (!res.ok) return 'inbox'
31
+ const payload = (await res.json()) as { note?: { tag?: string } }
32
+ return payload.note?.tag ?? 'inbox'
33
+ } catch {
34
+ return 'inbox'
35
+ }
36
+ }
37
+
38
+ export function mountNoteWidget(): () => void {
39
+ const root = document.createElement('div')
40
+ root.className = 'dsh-tw-note'
41
+
42
+ const card = document.createElement('div')
43
+ card.className = 'dsh-tw-note-card'
44
+ card.hidden = true
45
+
46
+ const head = document.createElement('div')
47
+ head.className = 'dsh-tw-note-head'
48
+ const label = document.createElement('span')
49
+ label.className = 'dsh-tw-note-label'
50
+ label.textContent = '📝 快速笔记'
51
+ const closeBtn = document.createElement('button')
52
+ closeBtn.type = 'button'
53
+ closeBtn.className = 'dsh-tw-note-close'
54
+ closeBtn.title = '收起'
55
+ closeBtn.textContent = '✕'
56
+ head.append(label, closeBtn)
57
+
58
+ const fields = document.createElement('div')
59
+ fields.className = 'dsh-tw-note-fields'
60
+ const titleInput = document.createElement('input')
61
+ titleInput.className = 'dsh-tw-note-title'
62
+ titleInput.placeholder = '标题(默认时间戳)'
63
+ const tagInput = document.createElement('input')
64
+ tagInput.className = 'dsh-tw-note-tag'
65
+ tagInput.placeholder = 'tag(默认 inbox)'
66
+ fields.append(titleInput, tagInput)
67
+
68
+ const textarea = document.createElement('textarea')
69
+ textarea.className = 'dsh-tw-note-text'
70
+ textarea.placeholder = '写点东西…(Ctrl+Enter 保存)'
71
+
72
+ const foot = document.createElement('div')
73
+ foot.className = 'dsh-tw-note-foot'
74
+ const hint = document.createElement('span')
75
+ hint.className = 'dsh-tw-note-hint'
76
+ hint.textContent = 'Ctrl+Enter'
77
+ const edit = document.createElement('button')
78
+ edit.type = 'button'
79
+ edit.className = 'dsh-tw-note-edit'
80
+ edit.title = '保存并在 TiddlyWiki 原生编辑器中打开'
81
+ edit.textContent = '✏️ 在 TW 中编辑'
82
+ const save = document.createElement('button')
83
+ save.type = 'button'
84
+ save.className = 'dsh-tw-note-save'
85
+ save.textContent = '保存'
86
+ foot.append(hint, edit, save)
87
+
88
+ card.append(head, fields, textarea, foot)
89
+
90
+ const toggle = document.createElement('button')
91
+ toggle.type = 'button'
92
+ toggle.className = 'dsh-tw-note-toggle'
93
+ toggle.textContent = '📝 快速笔记'
94
+
95
+ root.append(card, toggle)
96
+ document.body.append(root)
97
+
98
+ let defaultTag = 'inbox'
99
+ let opened = false
100
+
101
+ const resetTitle = (): void => {
102
+ titleInput.value = timestampTitle()
103
+ }
104
+
105
+ const open = async (): Promise<void> => {
106
+ card.hidden = false
107
+ opened = true
108
+ resetTitle()
109
+ if (tagInput.value.length === 0) tagInput.value = defaultTag
110
+ textarea.focus()
111
+ // Refresh the configured default tag on each open (cheap, best effort).
112
+ defaultTag = await fetchDefaultTag()
113
+ if (tagInput.value.length === 0) tagInput.value = defaultTag
114
+ }
115
+
116
+ const close = (): void => {
117
+ card.hidden = true
118
+ opened = false
119
+ }
120
+
121
+ toggle.addEventListener('click', () => { void (opened ? close() : open()) })
122
+ closeBtn.addEventListener('click', close)
123
+
124
+ const doSave = async (): Promise<void> => {
125
+ const text = textarea.value.trim()
126
+ if (text.length === 0) {
127
+ toast('内容为空,未保存')
128
+ return
129
+ }
130
+ save.disabled = true
131
+ save.textContent = '保存中…'
132
+ try {
133
+ const res = await fetch(NOTE_ENDPOINT, {
134
+ method: 'POST',
135
+ headers: { 'content-type': 'application/json' },
136
+ body: JSON.stringify({
137
+ title: titleInput.value.trim(),
138
+ tag: tagInput.value.trim(),
139
+ text,
140
+ }),
141
+ signal: AbortSignal.timeout(10_000),
142
+ })
143
+ const payload = (await res.json().catch(() => null)) as { ok?: boolean; title?: string; error?: string } | null
144
+ if (!res.ok || payload?.ok !== true) {
145
+ toast(`保存失败:${payload?.error ?? `HTTP ${res.status}`}`)
146
+ return
147
+ }
148
+ textarea.value = ''
149
+ resetTitle()
150
+ toast(`已保存「${payload.title ?? titleInput.value}」`)
151
+ close()
152
+ } catch (err) {
153
+ toast(`保存失败:${err instanceof Error ? err.message : String(err)}`)
154
+ } finally {
155
+ save.disabled = false
156
+ save.textContent = '保存'
157
+ }
158
+ }
159
+
160
+ save.addEventListener('click', () => { void doSave() })
161
+ textarea.addEventListener('keydown', (event) => {
162
+ if ((event.ctrlKey || event.metaKey) && event.key === 'Enter') {
163
+ event.preventDefault()
164
+ void doSave()
165
+ }
166
+ })
167
+
168
+ /** Save (if non-empty) and open the tiddler in TW's native editor. */
169
+ const doEdit = async (): Promise<void> => {
170
+ const title = titleInput.value.trim().length > 0 ? titleInput.value.trim() : timestampTitle()
171
+ const text = textarea.value
172
+ const tag = tagInput.value.trim()
173
+ edit.disabled = true
174
+ edit.textContent = '打开中…'
175
+ try {
176
+ const res = await fetch(EDIT_ENDPOINT, {
177
+ method: 'POST',
178
+ headers: { 'content-type': 'application/json' },
179
+ body: JSON.stringify({ title, tag, text }),
180
+ signal: AbortSignal.timeout(10_000),
181
+ })
182
+ const payload = (await res.json().catch(() => null)) as
183
+ | { ok?: boolean; title?: string; draftTitle?: string; twUrl?: string; error?: string }
184
+ | null
185
+ if (!res.ok || payload?.ok !== true) {
186
+ toast(`打开失败:${payload?.error ?? `HTTP ${res.status}`}`)
187
+ return
188
+ }
189
+ if (typeof payload.twUrl !== 'string' || typeof payload.draftTitle !== 'string') {
190
+ toast('打开失败:服务未返回编辑器地址')
191
+ return
192
+ }
193
+ openEditorPopup(`${payload.twUrl}#${encodeURIComponent(payload.draftTitle)}`, payload.title ?? title)
194
+ toast(`已在弹出窗口打开「${payload.title ?? title}」编辑器`)
195
+ } catch (err) {
196
+ toast(`打开失败:${err instanceof Error ? err.message : String(err)}`)
197
+ } finally {
198
+ edit.disabled = false
199
+ edit.textContent = '✏️ 在 TW 中编辑'
200
+ }
201
+ }
202
+
203
+ edit.addEventListener('click', () => { void doEdit() })
204
+
205
+ return () => {
206
+ root.remove()
207
+ const toastEl = document.querySelector<HTMLElement>('.dsh-tw-toast')
208
+ toastEl?.remove()
209
+ }
210
+ }