dsh-tiddlywiki 0.28.7 → 0.28.8

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.
Files changed (52) hide show
  1. package/README.md +1 -0
  2. package/docs/CHANGELOG.md +1 -0
  3. package/docs/features.md +4 -0
  4. package/docs/usage.md +13 -3
  5. package/lib/client.js +63 -27
  6. package/lib/index.js +10655 -10188
  7. package/lib/index.js.map +1 -1
  8. package/package.json +2 -2
  9. package/src/client/dom.ts +26 -0
  10. package/src/client/index.ts +14 -0
  11. package/src/client/note-widget-draft.ts +154 -0
  12. package/src/client/note-widget-tags.ts +184 -0
  13. package/src/client/note-widget-upload.ts +50 -0
  14. package/src/client/note-widget.ts +19 -348
  15. package/src/client/panel.ts +12 -3
  16. package/src/client/render-fetch.ts +11 -4
  17. package/src/client/scope-seat.ts +117 -0
  18. package/src/client/session-summary.ts +29 -7
  19. package/src/client/settings-icon-picker.ts +238 -0
  20. package/src/client/settings-page-catalog.ts +578 -0
  21. package/src/client/settings-page-config.ts +445 -0
  22. package/src/client/settings-page-runtime.ts +149 -0
  23. package/src/client/settings-page-wikis.ts +432 -0
  24. package/src/client/settings-page.ts +127 -1425
  25. package/src/client/styles.ts +36 -0
  26. package/src/client/tool-views.ts +170 -36
  27. package/src/client/wiki-icon.generated.ts +97 -0
  28. package/src/client/wiki-icon.ts +50 -15
  29. package/src/client/wiki-scope-dock.ts +56 -2
  30. package/src/client/wiki-scope.ts +98 -0
  31. package/src/host/admin-catalog.ts +488 -0
  32. package/src/host/admin-routes.ts +670 -0
  33. package/src/host/admin-secrets.ts +119 -0
  34. package/src/host/admin.ts +3 -1244
  35. package/src/host/clip-bridge.ts +24 -1
  36. package/src/host/config.ts +10 -2
  37. package/src/host/routes-session.ts +617 -0
  38. package/src/host/routes-tw-proxy.ts +390 -0
  39. package/src/host/routes.ts +77 -829
  40. package/src/host/tools-attach.ts +151 -0
  41. package/src/host/tools-git.ts +213 -0
  42. package/src/host/tools-notes.ts +672 -0
  43. package/src/host/tools-read.ts +421 -0
  44. package/src/host/tools-support.ts +513 -0
  45. package/src/host/tools.ts +33 -1814
  46. package/src/host/wiki-registry.ts +25 -4
  47. package/src/host/wiki.ts +55 -5
  48. package/src/index-clip.ts +142 -0
  49. package/src/index-git.ts +196 -0
  50. package/src/index-prompt.ts +139 -0
  51. package/src/index-wikis.ts +353 -0
  52. package/src/index.ts +89 -429
@@ -0,0 +1,238 @@
1
+ /**
2
+ * Per-wiki icon picker: the button + the paginated popup grid (v0.28.8).
3
+ *
4
+ * WHY IT IS ITS OWN MODULE: this is a self-contained widget (no settings-page
5
+ * state, no admin endpoints — the CALLER owns persisting the choice), and it
6
+ * started life inline in `settings-page.ts`, which was already the second-largest
7
+ * file in the repo. Splitting it keeps the settings page about settings.
8
+ *
9
+ * The set offered is exactly what `wiki-icon.ts` can render (the official DSH
10
+ * icon artwork, inlined, plus our own few glyphs) and exactly what the host
11
+ * accepts (`WIKI_ICON_NAMES`) — `scripts/verify-wiki-focus.mjs` asserts that
12
+ * three-way agreement, because a mismatch shows up as the silent, confusing
13
+ * "选了图标之后立刻变回默认".
14
+ *
15
+ * @module dsh-tiddlywiki/client/settings-icon-picker
16
+ */
17
+ import { make } from './dom.ts'
18
+ import { DEFAULT_ICON_SVG, ICON_NAMES, ICON_SVG } from './wiki-icon.ts'
19
+ /**
20
+ * Icon picker labels. The NAMES come from `wiki-icon.ts`, so the set the picker
21
+ * offers is exactly the set the sidebar can render.
22
+ *
23
+ * v0.28.8 (feedback item 1): the set is now the official DSH icon set (36 slugs,
24
+ * inlined from upstream artwork) plus our own 6 glyphs, so the picker can no
25
+ * longer be a `<select>` of ~8 emoji-ish options — long lists in a native select
26
+ * are hard to scan and cannot show the actual drawing. It is a POPUP GRID with
27
+ * pagination, where each cell renders the REAL icon (see `openIconPicker`).
28
+ */
29
+ const ICON_LABELS: Record<string, string> = {
30
+ // ── 官方 DSH 图标集(与上游组件一一对应,见 scripts/gen-wiki-icons.mjs)──
31
+ archive: '归档',
32
+ 'archive-check': '归档(已完成)',
33
+ 'archive-off': '取消归档',
34
+ unarchive: '取出归档',
35
+ database: '数据库',
36
+ data: '数据',
37
+ folder: '文件夹',
38
+ globe: '地球 / 公开',
39
+ branch: '分支',
40
+ list: '清单',
41
+ checklist: '核对清单',
42
+ 'flat-list': '平铺列表',
43
+ skill: '技能',
44
+ goal: '目标',
45
+ compact: '紧凑',
46
+ clock: '时钟 / 历史',
47
+ alarm: '提醒',
48
+ pin: '置顶',
49
+ shield: '安全 / 私密',
50
+ api: '接口',
51
+ code: '代码',
52
+ users: '团队',
53
+ user: '个人',
54
+ plugin: '插件',
55
+ pinwheel: '插件风车',
56
+ gauge: '仪表 / 用量',
57
+ tree: '工作区树',
58
+ personalization: '个性化',
59
+ link: '链接',
60
+ search: '检索',
61
+ settings: '设置',
62
+ sparkle: '灵感',
63
+ think: '思考',
64
+ light: '浅色',
65
+ dark: '深色',
66
+ refresh: '刷新',
67
+ // ── 自绘(上游没有对应图标)──
68
+ book: '书',
69
+ briefcase: '公文包',
70
+ home: '房子',
71
+ notebook: '笔记本',
72
+ flask: '实验',
73
+ star: '星标',
74
+ }
75
+
76
+ /** A few emoji that read well at sidebar size and are easy to tell apart. */
77
+ const EMOJI_CHOICES = ['📚', '💼', '🏠', '✍️', '🧪', '🌱', '🎯', '🗂️']
78
+
79
+ /**
80
+ * How many icons one page of the picker shows.
81
+ *
82
+ * 48 = 8 columns × 6 rows, which fits the popup without scrolling on a normal
83
+ * window while keeping the grid scannable. The set is 50+ entries, so pagination
84
+ * is what the author explicitly asked for ("太多的话可以考虑分页展示") rather
85
+ * than an endless scroll.
86
+ */
87
+ const ICON_PAGE_SIZE = 48
88
+
89
+ /** One selectable entry in the picker grid. */
90
+ interface IconChoice { value: string; label: string; kind: 'default' | 'name' | 'emoji' | 'custom' }
91
+
92
+ /**
93
+ * Every choice the picker offers, in display order: the default first (so
94
+ * "undo" is always the first cell), then the official/hand-drawn names, then a
95
+ * small emoji palette, and finally the user's own value when it is not already
96
+ * in the list (a hand-edited `wikis.json` emoji must never be silently dropped —
97
+ * that was an explicit rule of the original `<select>` implementation).
98
+ */
99
+ function iconChoices(current: string): IconChoice[] {
100
+ const choices: IconChoice[] = [{ value: '', label: '默认图标', kind: 'default' }]
101
+ for (const name of ICON_NAMES) choices.push({ value: name, label: ICON_LABELS[name] ?? name, kind: 'name' })
102
+ for (const emoji of EMOJI_CHOICES) choices.push({ value: emoji, label: emoji, kind: 'emoji' })
103
+ if (current.length > 0 && !choices.some((c) => c.value === current)) {
104
+ choices.push({ value: current, label: current, kind: 'custom' })
105
+ }
106
+ return choices
107
+ }
108
+
109
+ /**
110
+ * Render one icon value into an element, whatever KIND it is.
111
+ *
112
+ * Three cases, and the distinction matters: an empty value draws the built-in
113
+ * default; a built-in NAME draws our SVG (the only case where `innerHTML` is
114
+ * safe — the markup is ours); anything else is user text and must go through
115
+ * `textContent` (see applyWikiIcon's note about the control file being editable).
116
+ */
117
+ function paintIconValue(el: HTMLElement, value: string): void {
118
+ el.replaceChildren()
119
+ if (value.length === 0) {
120
+ el.innerHTML = DEFAULT_ICON_SVG
121
+ return
122
+ }
123
+ const svg = ICON_SVG[value]
124
+ if (svg !== undefined) {
125
+ el.innerHTML = svg
126
+ return
127
+ }
128
+ el.textContent = value
129
+ }
130
+
131
+ /** The row button that shows the current icon and opens the picker. */
132
+ export function makeIconButton(current: string, ariaLabel: string): HTMLButtonElement {
133
+ const btn = make('button', 'dsh-tw-settings-btn dsh-tw-settings-chipbtn dsh-tw-settings-iconbtn')
134
+ btn.type = 'button'
135
+ btn.setAttribute('aria-label', ariaLabel)
136
+ btn.title = current.length === 0 ? '选择入口图标(默认)' : `选择入口图标(当前:${current})`
137
+ const face = make('span', 'dsh-tw-settings-iconbtn-face')
138
+ paintIconValue(face, current)
139
+ btn.append(face, make('span', 'dsh-tw-settings-iconbtn-label', '图标'))
140
+ return btn
141
+ }
142
+
143
+ /**
144
+ * Open the paginated icon grid as a popup.
145
+ *
146
+ * Plain DOM (not React): the settings page is a vanilla-DOM tree, and DSH's own
147
+ * `Modal` primitive is a React component from a seed module we do not require.
148
+ * The popup closes on select, on Escape, and on an outside click; it is appended
149
+ * to `document.body` so no ancestor's `overflow` can clip it.
150
+ *
151
+ * @param anchor - the button the popup is positioned under.
152
+ * @param current - the wiki's current icon value.
153
+ * @param onPick - called with the new value (the caller persists + re-renders).
154
+ */
155
+ export function openIconPicker(anchor: HTMLElement, current: string, onPick: (next: string) => void): void {
156
+ // Only one popup at a time: clicking a second row's 图标 must move it, not
157
+ // stack another layer on top of the first.
158
+ document.querySelector('.dsh-tw-iconpicker')?.remove()
159
+
160
+ const choices = iconChoices(current)
161
+ const pages = Math.max(1, Math.ceil(choices.length / ICON_PAGE_SIZE))
162
+ let page = Math.max(0, Math.floor(choices.findIndex((c) => c.value === current) / ICON_PAGE_SIZE))
163
+
164
+ const popup = make('div', 'dsh-tw-iconpicker')
165
+ popup.setAttribute('role', 'dialog')
166
+ popup.setAttribute('aria-label', '选择入口图标')
167
+ const grid = make('div', 'dsh-tw-iconpicker-grid')
168
+ const pager = make('div', 'dsh-tw-iconpicker-pager')
169
+ popup.append(grid, pager)
170
+
171
+ const close = (): void => {
172
+ popup.remove()
173
+ document.removeEventListener('keydown', onKeyDown, true)
174
+ document.removeEventListener('mousedown', onOutside, true)
175
+ }
176
+ const onKeyDown = (event: KeyboardEvent): void => {
177
+ if (event.key === 'Escape') {
178
+ event.stopPropagation()
179
+ close()
180
+ }
181
+ }
182
+ const onOutside = (event: MouseEvent): void => {
183
+ const target = event.target
184
+ if (target instanceof Node && !popup.contains(target) && !anchor.contains(target)) close()
185
+ }
186
+
187
+ const paintPage = (): void => {
188
+ grid.replaceChildren()
189
+ const slice = choices.slice(page * ICON_PAGE_SIZE, (page + 1) * ICON_PAGE_SIZE)
190
+ for (const choice of slice) {
191
+ const cell = make('button', 'dsh-tw-iconpicker-cell')
192
+ cell.type = 'button'
193
+ cell.title = choice.label
194
+ cell.setAttribute('aria-label', choice.label)
195
+ if (choice.value === current) cell.dataset.current = 'true'
196
+ const face = make('span', 'dsh-tw-iconpicker-face')
197
+ paintIconValue(face, choice.value)
198
+ cell.append(face, make('span', 'dsh-tw-iconpicker-name', choice.label))
199
+ cell.addEventListener('click', () => {
200
+ close()
201
+ onPick(choice.value)
202
+ })
203
+ grid.append(cell)
204
+ }
205
+ pager.replaceChildren()
206
+ if (pages > 1) {
207
+ const prev = make('button', 'dsh-tw-settings-btn dsh-tw-settings-chipbtn', '‹ 上一页')
208
+ prev.type = 'button'
209
+ prev.disabled = page === 0
210
+ prev.addEventListener('click', () => { page -= 1; paintPage() })
211
+ const info = make('span', 'dsh-tw-settings-muted', `第 ${page + 1} / ${pages} 页 · 共 ${choices.length} 个`)
212
+ const next = make('button', 'dsh-tw-settings-btn dsh-tw-settings-chipbtn', '下一页 ›')
213
+ next.type = 'button'
214
+ next.disabled = page >= pages - 1
215
+ next.addEventListener('click', () => { page += 1; paintPage() })
216
+ pager.append(prev, info, next)
217
+ } else {
218
+ pager.append(make('span', 'dsh-tw-settings-muted', `共 ${choices.length} 个图标`))
219
+ }
220
+ }
221
+ paintPage()
222
+
223
+ document.body.append(popup)
224
+ // Position under the button, clamped into the viewport, and FLIP upward when
225
+ // there is not enough room below (the wiki list can sit near the page bottom).
226
+ const rect = anchor.getBoundingClientRect()
227
+ const height = popup.offsetHeight
228
+ const spaceBelow = window.innerHeight - rect.bottom
229
+ const top = spaceBelow < height + 12 && rect.top > height + 12
230
+ ? rect.top - height - 6
231
+ : rect.bottom + 6
232
+ popup.style.top = `${Math.max(6, Math.min(top, window.innerHeight - 12))}px`
233
+ popup.style.left = `${Math.max(6, Math.min(rect.left, window.innerWidth - popup.offsetWidth - 6))}px`
234
+
235
+ document.addEventListener('keydown', onKeyDown, true)
236
+ document.addEventListener('mousedown', onOutside, true)
237
+ }
238
+