dsh-tiddlywiki 0.1.2 → 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.
@@ -24,6 +24,21 @@ const CSS_TEXT = `
24
24
  .dsh-tw-entry[data-active="true"] { background: var(--dsw-alias-interactive-bg-active, rgba(128,128,128,.18)); color: var(--dsw-alias-label-primary, inherit); font-weight: 500; }
25
25
  .dsh-tw-entry svg { flex: none; }
26
26
  .dsh-tw-entry .dsh-tw-entry-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
27
+ /* Collapsed rail: the shell narrows the sidebar to a 56px icon rail (dual
28
+ signals — the frame's data-sidebar-collapsed + the sidebar root's hashed
29
+ *_collapsed class, same doctrine as dsh-taskboard 0.4.3). Mirror the
30
+ native 36×36 icon-button geometry: center the icon, hide the label, and
31
+ scale it up to match the shell's 18px rail icons. */
32
+ [data-sidebar-collapsed] [data-dsh-tw-entry],
33
+ [class*="_collapsed"] [data-dsh-tw-entry] {
34
+ width: 36px; height: 36px; min-width: 36px;
35
+ margin: 0 0 12px; padding: 0;
36
+ justify-content: center; gap: 0; text-align: center;
37
+ }
38
+ [data-sidebar-collapsed] [data-dsh-tw-entry] .dsh-tw-entry-label,
39
+ [class*="_collapsed"] [data-dsh-tw-entry] .dsh-tw-entry-label { display: none; }
40
+ [data-sidebar-collapsed] [data-dsh-tw-entry] svg,
41
+ [class*="_collapsed"] [data-dsh-tw-entry] svg { width: 18px; height: 18px; }
27
42
 
28
43
  /* ── center-column panel (fixed overlay, JS-pinned to the column rect) ── */
29
44
  .dsh-tw-view {
@@ -139,6 +154,34 @@ html[data-dsh-tw-active] .dshDesktopConversationSurface > :not([data-dsh-tw-view
139
154
  }
140
155
  .dsh-tw-note-edit:hover:not(:disabled) { filter: brightness(.96); }
141
156
  .dsh-tw-note-edit:disabled { opacity: .55; cursor: default; }
157
+ /* ── Multi-tag chip editor (quick-note tags) ─────────────────────────────── */
158
+ .dsh-tw-note-tags { position: relative; flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 4px; }
159
+ .dsh-tw-note-chips { display: flex; flex-wrap: wrap; gap: 4px; }
160
+ .dsh-tw-note-tagchip {
161
+ display: inline-flex; align-items: center; gap: 4px;
162
+ padding: 2px 6px 2px 8px; border-radius: 999px; font-size: 11px; line-height: 1.4;
163
+ background: color-mix(in srgb, var(--dsw-alias-brand-primary, #3e63dd) 10%, var(--dsw-alias-bg-layer-1, #fff));
164
+ border: 1px solid color-mix(in srgb, var(--dsw-alias-brand-primary, #3e63dd) 30%, transparent);
165
+ color: var(--dsw-alias-label-primary, #222);
166
+ }
167
+ .dsh-tw-note-tagchip-x {
168
+ cursor: pointer; font-size: 12px; line-height: 1; padding: 0 2px; border-radius: 50%;
169
+ color: var(--dsw-alias-label-secondary, #888);
170
+ }
171
+ .dsh-tw-note-tagchip-x:hover { color: var(--dsw-alias-state-error-primary, #d13b3b); }
172
+ .dsh-tw-note-taginput { width: 100%; box-sizing: border-box; }
173
+ .dsh-tw-note-tagsuggest {
174
+ position: absolute; left: 0; right: 0; top: calc(100% + 2px); z-index: 60;
175
+ max-height: 168px; overflow-y: auto; border-radius: 8px; padding: 4px;
176
+ background: var(--dsw-alias-bg-layer-2, #fff); color: var(--dsw-alias-label-primary, #222);
177
+ border: 1px solid var(--dsw-alias-border-l2, rgba(0,0,0,.18));
178
+ box-shadow: var(--dsw-shadow-lv2, 0 4px 16px rgba(0,0,0,.16));
179
+ font-size: 12px;
180
+ }
181
+ .dsh-tw-note-tagsuggest-item {
182
+ padding: 6px 9px; border-radius: 6px; cursor: pointer; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
183
+ }
184
+ .dsh-tw-note-tagsuggest-item:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,.12)); }
142
185
  /* ── Native-editor popup iframe (quick-note "在 TW 中编辑") ──────────────── */
143
186
  .dsh-tw-editor-popup {
144
187
  position: fixed; left: 0; right: 0; top: 0; bottom: 0; margin: auto;
@@ -20,6 +20,12 @@ export const CONFIG_TIDDLER = '$:/plugins/dsh-tiddlywiki/config'
20
20
  export interface PluginConfigShape {
21
21
  note?: { tag?: string }
22
22
  git?: { autoCommit?: boolean; debounceMs?: number; remote?: string; branch?: string }
23
+ ui?: {
24
+ /** 是否在界面右下角显示「快速笔记」悬浮按钮(默认 true)。 */
25
+ showQuickNote?: boolean
26
+ /** 是否启用「侧边移到左侧.css」默认 CSS;被删除后随启动补丁还原(默认 true)。 */
27
+ sidebarLeftCss?: boolean
28
+ }
23
29
  uiLanguage?: string
24
30
  [key: string]: unknown
25
31
  }
@@ -39,6 +39,7 @@ export interface RouteDeps {
39
39
  git: GitFace
40
40
  autoCommit: () => void
41
41
  noteDefaults: () => { tag: string }
42
+ uiDefaults: () => { showQuickNote: boolean }
42
43
  getWikiPath: () => string
43
44
  }
44
45
 
@@ -86,10 +87,10 @@ export async function openInTwEditor(
86
87
  client: TiddlyWebClient,
87
88
  title: string,
88
89
  text: string,
89
- tag: string,
90
+ tags: string[],
90
91
  ): Promise<{ title: string; draftTitle: string }> {
91
92
  if (text.trim().length > 0) {
92
- await client.put({ title, text, tags: [tag] })
93
+ await client.put({ title, text, tags })
93
94
  }
94
95
  // Draft content: the provided text, else the existing tiddler's content.
95
96
  let draftText = text
@@ -115,6 +116,22 @@ export async function openInTwEditor(
115
116
  return { title, draftTitle }
116
117
  }
117
118
 
119
+ /** Resolve note tags from the request body: `tags` array wins, then the
120
+ * legacy single `tag` string, then the configured default tag. */
121
+ function resolveTags(
122
+ body: { tag?: unknown; tags?: unknown },
123
+ defaultTag: string,
124
+ ): string[] {
125
+ if (Array.isArray(body.tags)) {
126
+ const tags = body.tags.filter((t): t is string => typeof t === 'string' && t.trim().length > 0).map((t) => t.trim())
127
+ if (tags.length > 0) return tags
128
+ }
129
+ if (typeof body.tag === 'string' && body.tag.trim().length > 0) {
130
+ return body.tag.trim().split(/\s+/).filter(Boolean)
131
+ }
132
+ return [defaultTag]
133
+ }
134
+
118
135
  export function registerRoutes(ctx: { webServer: WebServerFace }, deps: RouteDeps): () => void {
119
136
  const handleStatus = async (_req: IncomingMessage, res: ServerResponse): Promise<void> => {
120
137
  const view = deps.server.status()
@@ -124,12 +141,12 @@ export function registerRoutes(ctx: { webServer: WebServerFace }, deps: RouteDep
124
141
  } catch {
125
142
  gitSummary = null
126
143
  }
127
- json(res, { ok: true, ...view, git: gitSummary, note: { tag: deps.noteDefaults().tag } })
144
+ json(res, { ok: true, ...view, git: gitSummary, note: { tag: deps.noteDefaults().tag }, ui: deps.uiDefaults() })
128
145
  }
129
146
 
130
147
  const handleNote = async (req: IncomingMessage, res: ServerResponse): Promise<void> => {
131
148
  try {
132
- const body = JSON.parse(await readBody(req)) as { title?: unknown; tag?: unknown; text?: unknown }
149
+ const body = JSON.parse(await readBody(req)) as { title?: unknown; tag?: unknown; tags?: unknown; text?: unknown }
133
150
  const text = typeof body.text === 'string' && body.text.trim().length > 0 ? body.text.trim() : null
134
151
  if (text === null) {
135
152
  json(res, { ok: false, error: 'text is required' }, 400)
@@ -141,10 +158,10 @@ export function registerRoutes(ctx: { webServer: WebServerFace }, deps: RouteDep
141
158
  return
142
159
  }
143
160
  const title = typeof body.title === 'string' && body.title.trim().length > 0 ? body.title.trim() : timestampTitle()
144
- const tag = typeof body.tag === 'string' && body.tag.trim().length > 0 ? body.tag.trim() : deps.noteDefaults().tag
145
- await client.put({ title, text, tags: [tag] })
161
+ const tags = resolveTags(body, deps.noteDefaults().tag)
162
+ await client.put({ title, text, tags })
146
163
  deps.autoCommit()
147
- json(res, { ok: true, title, tag, text })
164
+ json(res, { ok: true, title, tag: tags.join(' '), tags, text })
148
165
  } catch (err) {
149
166
  json(res, { ok: false, error: err instanceof Error ? err.message : String(err) }, 500)
150
167
  }
@@ -152,16 +169,16 @@ export function registerRoutes(ctx: { webServer: WebServerFace }, deps: RouteDep
152
169
 
153
170
  const handleEdit = async (req: IncomingMessage, res: ServerResponse): Promise<void> => {
154
171
  try {
155
- const body = JSON.parse(await readBody(req)) as { title?: unknown; tag?: unknown; text?: unknown }
172
+ const body = JSON.parse(await readBody(req)) as { title?: unknown; tag?: unknown; tags?: unknown; text?: unknown }
156
173
  const client = deps.getClient()
157
174
  if (client === undefined) {
158
175
  json(res, { ok: false, error: 'wiki service is not running' }, 503)
159
176
  return
160
177
  }
161
178
  const title = typeof body.title === 'string' && body.title.trim().length > 0 ? body.title.trim() : timestampTitle()
162
- const tag = typeof body.tag === 'string' && body.tag.trim().length > 0 ? body.tag.trim() : deps.noteDefaults().tag
179
+ const tags = resolveTags(body, deps.noteDefaults().tag)
163
180
  const text = typeof body.text === 'string' ? body.text : ''
164
- const result = await openInTwEditor(client, title, text, tag)
181
+ const result = await openInTwEditor(client, title, text, tags)
165
182
  deps.autoCommit()
166
183
  json(res, { ok: true, ...result, twUrl: deps.server.url })
167
184
  } catch (err) {
@@ -169,6 +186,27 @@ export function registerRoutes(ctx: { webServer: WebServerFace }, deps: RouteDep
169
186
  }
170
187
  }
171
188
 
189
+ /** Distinct non-system tags for the quick-note tag autocomplete. */
190
+ const handleTags = async (_req: IncomingMessage, res: ServerResponse): Promise<void> => {
191
+ const client = deps.getClient()
192
+ if (client === undefined) {
193
+ json(res, { ok: false, error: 'wiki service is not running' }, 503)
194
+ return
195
+ }
196
+ try {
197
+ const items = await client.list(undefined, false)
198
+ const tags = new Set<string>()
199
+ for (const item of items) {
200
+ for (const tag of item.tags ?? []) {
201
+ if (tag.length > 0 && !tag.startsWith('$:/')) tags.add(tag)
202
+ }
203
+ }
204
+ json(res, { ok: true, tags: [...tags].sort((a, b) => a.localeCompare(b, 'zh')) })
205
+ } catch (err) {
206
+ json(res, { ok: false, error: err instanceof Error ? err.message : String(err) }, 500)
207
+ }
208
+ }
209
+
172
210
  const handleRestart = async (_req: IncomingMessage, res: ServerResponse): Promise<void> => {
173
211
  try {
174
212
  await deps.server.restart()
@@ -212,6 +250,7 @@ export function registerRoutes(ctx: { webServer: WebServerFace }, deps: RouteDep
212
250
  ctx.webServer.register({ kind: 'exact', path: `${ROUTE_PREFIX}/status`, handler: (req, res) => { void handleStatus(req, res) } }),
213
251
  ctx.webServer.register({ kind: 'exact', path: `${ROUTE_PREFIX}/note`, handler: (req, res) => { void handleNote(req, res) } }),
214
252
  ctx.webServer.register({ kind: 'exact', path: `${ROUTE_PREFIX}/edit`, handler: (req, res) => { void handleEdit(req, res) } }),
253
+ ctx.webServer.register({ kind: 'exact', path: `${ROUTE_PREFIX}/tags`, handler: (req, res) => { void handleTags(req, res) } }),
215
254
  ctx.webServer.register({ kind: 'exact', path: `${ROUTE_PREFIX}/restart`, handler: (req, res) => { void handleRestart(req, res) } }),
216
255
  ctx.webServer.register({ kind: 'prefix', path: `${ROUTE_PREFIX}/api`, handler: (req, res) => { void handleApiProxy(req, res) } }),
217
256
  ]
@@ -1,10 +1,16 @@
1
1
  /**
2
- * Built-in doc note for the plugin (design doc §14): a short user-facing
3
- * "how to use dsh-tiddlywiki" note that is seeded into the wiki on first run.
2
+ * Built-in doc note + optional UI-patch seeds for the plugin (design doc §14).
4
3
  *
5
- * Idempotent seed (create-if-missing): on every plugin start we check whether
6
- * the note tiddler exists and only write it when it is absent — deleting it
7
- * and restarting dsh web recreates it, but editing it never gets overwritten.
4
+ * Two seeds, deliberately different lifetimes:
5
+ *
6
+ * 1. DOC NOTE (`dsh-tiddlywiki 插件说明`) — ONE-SHOT: seeded only the first
7
+ * time this plugin runs on a wiki (guarded by a `seed-doc-note` marker
8
+ * tiddler). After that the note is the user's own content: deleting it and
9
+ * restarting dsh web does NOT recreate it.
10
+ * 2. SIDEBAR-LEFT CSS (`侧边移到左侧.css`) — PATCH-REPAIR: written whenever
11
+ * the wiki lacks it while the `ui.sidebarLeftCss` option is enabled
12
+ * (default on). The user may delete it; a restart with the option still on
13
+ * re-seeds it (the "补丁选项"). Turning the option off stops re-seeding.
8
14
  *
9
15
  * @module dsh-tiddlywiki/host/seed-notes
10
16
  */
@@ -16,6 +22,9 @@ export const DOC_NOTE_TITLE = 'dsh-tiddlywiki 插件说明'
16
22
  /** Tag that makes the note easy to find via `tiddlywiki_search tag=docs`. */
17
23
  export const DOC_NOTE_TAG = 'docs'
18
24
 
25
+ /** One-time marker: its presence means "the note was offered once — hands off". */
26
+ export const SEED_MARKER_TITLE = '$:/plugins/dsh-tiddlywiki/seed-doc-note'
27
+
19
28
  /** The note body, TiddlyWiki wiki-text. */
20
29
  export const DOC_NOTE_TEXT = `! dsh-tiddlywiki 插件说明
21
30
 
@@ -25,9 +34,9 @@ export const DOC_NOTE_TEXT = `! dsh-tiddlywiki 插件说明
25
34
 
26
35
  * **5 个 agent 工具**:\`tiddlywiki_search\`(检索)/ \`tiddlywiki_get\`(读)/ \`tiddlywiki_put\`(写)/ \`tiddlywiki_delete\`(删)/ \`tiddlywiki_git_sync\`(git 同步)。
27
36
  * **TW 编辑器面板**:侧边栏「TiddlyWiki」按钮 → 在界面中央打开完整版 TW 编辑器。
28
- * **快速笔记**:右下角悬浮「📝 快速笔记」写随手记,\`Ctrl+Enter\` 保存;点「✏️ 在 TW 中编辑」会弹出独立小窗用 TW 原生编辑器编辑。
37
+ * **快速笔记**:右下角悬浮「📝 快速笔记」写随手记(可多选/自动补全 tag),\`Ctrl+Enter\` 保存;点「✏️ 在 TW 中编辑」会弹出独立小窗用 TW 原生编辑器编辑。
29
38
  * **git 同步**:写入自动防抖 commit(默认 60 秒);手动 \`tiddlywiki_git_sync action=sync\` 做 pull → commit → push。
30
- * **设置页**:DSH 设置 → 「TiddlyWiki 知识库」管理插件/主题/语言与运行配置。
39
+ * **设置页**:DSH 设置 → 「TiddlyWiki 知识库」管理插件/主题/语言与运行配置(含「快速笔记」显示开关、侧边栏移左 CSS 补丁开关)。
31
40
 
32
41
  !! 知识库纪律(三条)
33
42
 
@@ -42,21 +51,72 @@ export const DOC_NOTE_TEXT = `! dsh-tiddlywiki 插件说明
42
51
 
43
52
  !! 说明
44
53
 
45
- * 本笔记由插件在首次启动时自动写入 wiki(幂等:不存在才写)。删除后重启 dsh web 会重建;手动编辑过的内容不会被覆盖。
54
+ * 本笔记由插件在**首次启动**时自动写入(一次性:只写一次)。删除后重启 dsh web **不会自动恢复**——它从此归你所有。
55
+ * 「\`侧边移到左侧.css\`」是插件默认安装的界面补丁(把 TW 右侧栏移到左边);可在设置页关闭「侧边栏移左 CSS」开关来停用补丁还原。
46
56
  * 更多细节见插件仓库 README。`
47
57
 
48
58
  /**
49
- * Seed the doc note when it is absent. Returns whether a note was written.
50
- * Never throws (missing wiki or note already present → no-op / false).
59
+ * Seed the doc note exactly once per wiki. A marker tiddler records that the
60
+ * note has been offered; from then on the note is user-owned and is never
61
+ * re-created (deleting it survives restarts). Returns whether a note was
62
+ * written this call. Never throws.
51
63
  */
52
64
  export async function seedDocNote(client: TiddlyWebClient): Promise<boolean> {
65
+ const marker = await client.get(SEED_MARKER_TITLE).catch(() => undefined)
66
+ if (marker !== undefined) return false
53
67
  const existing = await client.get(DOC_NOTE_TITLE).catch(() => undefined)
68
+ let wrote = false
69
+ if (existing === undefined) {
70
+ await client.put({
71
+ title: DOC_NOTE_TITLE,
72
+ text: DOC_NOTE_TEXT,
73
+ type: 'text/vnd.tiddlywiki',
74
+ tags: [DOC_NOTE_TAG],
75
+ })
76
+ wrote = true
77
+ }
78
+ // Record the offer regardless, so an existing note (upgrade from an older
79
+ // create-if-missing version) also becomes user-owned from here on.
80
+ await client
81
+ .put({ title: SEED_MARKER_TITLE, text: 'seeded-once', type: 'text/plain', tags: [] })
82
+ .catch(() => undefined)
83
+ return wrote
84
+ }
85
+
86
+ /** Sidebar-left CSS tiddler: moves TW's sidebar to the LEFT (mirror of the
87
+ * `centralised` default), so dsh-better-sidebar's top-right toggle cluster
88
+ * no longer sits over TW's own right sidebar. A normal tiddler (tag
89
+ * `$:/tags/Stylesheet`, type text/css) — same pattern as hand-made
90
+ * `编辑界面微调:小标题·大编辑器.css`, easy to find and delete. */
91
+ export const SIDEBAR_LEFT_CSS_TITLE = '侧边移到左侧.css'
92
+
93
+ /** The stylesheet body. Hardcoded 960px breakpoint (vanilla default
94
+ * sidebarbreakpoint) — plain text/css tiddlers don't process `{{...}}`. */
95
+ export const SIDEBAR_LEFT_CSS = `/* 由 dsh-tiddlywiki 插件默认安装:把 TW 右侧栏镜像到左侧(与 centralised 主题默认对称)。
96
+ 删除后,只要设置页「侧边栏移左 CSS」补丁开关开着,插件会在下次启动时重建本 tiddler。 */
97
+ @media (min-width: 960px) {
98
+ html .tc-sidebar-scrollable {
99
+ left: 0 !important;
100
+ right: 50% !important;
101
+ margin-left: 0 !important;
102
+ margin-right: 343px !important;
103
+ }
104
+ }`
105
+
106
+ /**
107
+ * Patch-repair the sidebar-left CSS while the option is enabled: write it
108
+ * whenever it is missing (fresh installs and deleted-by-user both re-seed).
109
+ * Returns whether it was written. Never throws.
110
+ */
111
+ export async function seedSidebarLeftCss(client: TiddlyWebClient, enabled: boolean): Promise<boolean> {
112
+ if (!enabled) return false
113
+ const existing = await client.get(SIDEBAR_LEFT_CSS_TITLE).catch(() => undefined)
54
114
  if (existing !== undefined) return false
55
115
  await client.put({
56
- title: DOC_NOTE_TITLE,
57
- text: DOC_NOTE_TEXT,
58
- type: 'text/vnd.tiddlywiki',
59
- tags: [DOC_NOTE_TAG],
116
+ title: SIDEBAR_LEFT_CSS_TITLE,
117
+ text: SIDEBAR_LEFT_CSS,
118
+ type: 'text/css',
119
+ tags: ['$:/tags/Stylesheet'],
60
120
  })
61
121
  return true
62
122
  }
package/src/index.ts CHANGED
@@ -25,7 +25,7 @@ import { AutoCommitter, GitFace } from './host/git.ts'
25
25
  import { registerRoutes, type WebServerFace } from './host/routes.ts'
26
26
  import { ConfigStore, deepMerge, type PluginConfigShape } from './host/config.ts'
27
27
  import { registerAdminRoutes, ensureLanguage, resolveTwRoot, type AdminDeps } from './host/admin.ts'
28
- import { seedDocNote, DOC_NOTE_TITLE } from './host/seed-notes.ts'
28
+ import { seedDocNote, seedSidebarLeftCss, DOC_NOTE_TITLE } from './host/seed-notes.ts'
29
29
  import { TiddlyWebClient } from './host/tw-api.ts'
30
30
  import { registerTiddlywikiTools, type ToolsDeps } from './host/tools.ts'
31
31
  import { PATH_PREFIX, WikiServer, type WikiServerOptions } from './host/wiki.ts'
@@ -42,7 +42,7 @@ export { AutoCommitter, GitFace, PATH_PREFIX, TiddlyWebClient, WikiServer, dshHo
42
42
  export { ConfigStore, deepMerge } from './host/config.ts'
43
43
  export { openInTwEditor } from './host/routes.ts'
44
44
  export { registerAdminRoutes, resolveTwRoot, readWikiInfo, writeWikiInfo, bundledCatalog, ensureLanguage, normalizeThemes } from './host/admin.ts'
45
- export { seedDocNote, DOC_NOTE_TITLE, DOC_NOTE_TAG, DOC_NOTE_TEXT } from './host/seed-notes.ts'
45
+ export { seedDocNote, seedSidebarLeftCss, DOC_NOTE_TITLE, DOC_NOTE_TAG, DOC_NOTE_TEXT, SIDEBAR_LEFT_CSS_TITLE, SIDEBAR_LEFT_CSS } from './host/seed-notes.ts'
46
46
  export type { PluginConfigShape } from './host/config.ts'
47
47
  export type { GitStatusView } from './host/git.ts'
48
48
  export type { Tiddler } from './host/tw-api.ts'
@@ -55,6 +55,7 @@ export interface TiddlywikiConfig {
55
55
  port?: number
56
56
  git?: { autoCommit?: boolean; debounceMs?: number; remote?: string; branch?: string }
57
57
  note?: { tag?: string }
58
+ ui?: { showQuickNote?: boolean; sidebarLeftCss?: boolean }
58
59
  auth?: { username?: string; password?: string }
59
60
  }
60
61
 
@@ -75,6 +76,7 @@ interface ResolvedConfig {
75
76
  port: number
76
77
  git: { autoCommit: boolean; debounceMs: number; remote: string; branch: string }
77
78
  note: { tag: string }
79
+ ui: { showQuickNote: boolean; sidebarLeftCss: boolean }
78
80
  auth: { username?: string; password?: string }
79
81
  }
80
82
 
@@ -84,6 +86,7 @@ const DEFAULTS: ResolvedConfig = {
84
86
  port: 0,
85
87
  git: { autoCommit: true, debounceMs: 60_000, remote: '', branch: 'main' },
86
88
  note: { tag: 'inbox' },
89
+ ui: { showQuickNote: true, sidebarLeftCss: true },
87
90
  auth: { username: '', password: '' },
88
91
  }
89
92
 
@@ -163,6 +166,7 @@ export function apply(ctx: HostCtx, rawConfig: TiddlywikiConfig = {}): void {
163
166
  port: rawConfig.port ?? DEFAULTS.port,
164
167
  git: { ...DEFAULTS.git, ...(rawConfig.git ?? {}) },
165
168
  note: { ...DEFAULTS.note, ...(rawConfig.note ?? {}) },
169
+ ui: { ...DEFAULTS.ui, ...(rawConfig.ui ?? {}) },
166
170
  auth: { ...DEFAULTS.auth, ...(rawConfig.auth ?? {}) },
167
171
  }
168
172
  const wikiPath = join(config.wikiRoot, config.wiki)
@@ -171,12 +175,16 @@ export function apply(ctx: HostCtx, rawConfig: TiddlywikiConfig = {}): void {
171
175
  // Runtime-editable config (settings page): the cordis `config:` block is the
172
176
  // BASE; a config tiddler ($:/plugins/dsh-tiddlywiki/config) written by the
173
177
  // settings page overlays it. Effective values come from configStore.get().
174
- const configStore = new ConfigStore({ note: config.note, git: config.git } satisfies PluginConfigShape)
178
+ const configStore = new ConfigStore({ note: config.note, git: config.git, ui: config.ui } satisfies PluginConfigShape)
175
179
  const eff = (): PluginConfigShape => configStore.get()
176
180
  const effectiveNoteTag = (): string => {
177
181
  const tag = eff().note?.tag
178
182
  return typeof tag === 'string' && tag.trim().length > 0 ? tag : config.note.tag
179
183
  }
184
+ const effectiveUi = (): { showQuickNote: boolean; sidebarLeftCss: boolean } => ({
185
+ showQuickNote: eff().ui?.showQuickNote !== false,
186
+ sidebarLeftCss: eff().ui?.sidebarLeftCss !== false,
187
+ })
180
188
 
181
189
  const disposers: Array<() => void> = []
182
190
  const disposeAll = (): void => {
@@ -265,13 +273,19 @@ export function apply(ctx: HostCtx, rawConfig: TiddlywikiConfig = {}): void {
265
273
  try {
266
274
  await server.start()
267
275
  await configStore.load(client())
268
- // Seed the built-in doc note (idempotent, create-if-missing) so a fresh
269
- // wiki gets the plugin guide by default.
276
+ // Seeds (run after the effective config is loaded):
277
+ // - doc note: ONE-SHOT — a fresh wiki gets the plugin guide; deleting
278
+ // it afterwards survives restarts (see seedDocNote).
279
+ // - sidebar-left CSS: PATCH-REPAIR while ui.sidebarLeftCss is on —
280
+ // re-seeds when missing (fresh install OR user deleted it).
270
281
  try {
271
282
  const seedClient = client()
272
- if (seedClient !== undefined) await seedDocNote(seedClient)
283
+ if (seedClient !== undefined) {
284
+ await seedDocNote(seedClient)
285
+ await seedSidebarLeftCss(seedClient, effectiveUi().sidebarLeftCss)
286
+ }
273
287
  } catch (err) {
274
- console.warn('[dsh-tiddlywiki] seeding doc note:', err)
288
+ console.warn('[dsh-tiddlywiki] seeding doc note / css:', err)
275
289
  }
276
290
  // Apply the configured UI language (e.g. "zh-Hans"): enable the bundled
277
291
  // language plugin in tiddlywiki.info.languages + restart once so TW loads
@@ -307,6 +321,7 @@ export function apply(ctx: HostCtx, rawConfig: TiddlywikiConfig = {}): void {
307
321
  git,
308
322
  autoCommit: () => committer?.touch(),
309
323
  noteDefaults: () => ({ tag: effectiveNoteTag() }),
324
+ uiDefaults: () => effectiveUi(),
310
325
  getWikiPath: () => wikiPath,
311
326
  })
312
327
  const adminDeps: AdminDeps = {