dsh-tiddlywiki 0.4.0 → 0.6.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.
@@ -10,10 +10,13 @@
10
10
  * Toggling rides the shared PanelState; cross-plugin exclusivity rides the
11
11
  * `dsh-panel-activate` event (same protocol as dsh-taskboard).
12
12
  *
13
- * The iframe points DIRECTLY at the TW service (http://127.0.0.1:<port>),
14
- * bypassing reverse-proxy sensitivity (design doc R1 — proxy is only a
15
- * fallback). The panel reads /status first and only sets iframe.src when the
16
- * service is actually running.
13
+ * The iframe points at the SAME-ORIGIN TW proxy (`<origin>/dsh-tiddlywiki/tw/`,
14
+ * remote-access mode R1): the browser only talks to the DSH origin — which it
15
+ * already reaches over loopback, LAN, Tailscale, a domain or HTTPS — and DSH
16
+ * proxies to the loopback TW child. The panel reads /status first and only
17
+ * sets iframe.src when the service is actually running. Because the proxy URL
18
+ * is origin-relative and port-independent, a TW restart never reloads the
19
+ * iframe, so an in-progress edit keeps its unsaved state.
17
20
  *
18
21
  * @module dsh-tiddlywiki/client/panel
19
22
  */
@@ -76,6 +79,8 @@ interface StatusPayload {
76
79
  ok?: boolean
77
80
  status: string
78
81
  url?: string
82
+ /** Same-origin TW proxy path (e.g. /dsh-tiddlywiki/tw/); the iframe base. */
83
+ twProxy?: string
79
84
  wikiPath?: string
80
85
  error?: string
81
86
  note?: { tag?: string }
@@ -115,9 +120,6 @@ export function mountPanel(state: PanelState): () => void {
115
120
  let iframe: HTMLIFrameElement | undefined
116
121
  let frameArea: HTMLDivElement | undefined
117
122
  let errorArea: HTMLDivElement | undefined
118
- let chip: HTMLSpanElement | undefined
119
- let statusDot: HTMLSpanElement | undefined
120
- let statusFloater: HTMLDivElement | undefined
121
123
  let refreshTimer: number | undefined
122
124
  let refreshAttempts = 0
123
125
 
@@ -143,48 +145,6 @@ export function mountPanel(state: PanelState): () => void {
143
145
  return view
144
146
  }
145
147
 
146
- /**
147
- * Bottom-right floating status/reload buttons (same visual language as the
148
- * quick-note toggle) — the top panel bar is gone, so TW's menubar reaches
149
- * the very top of the panel and clears the host's top-right buttons. Mount
150
- * is gated by `ui.showPanelStatus` (settings page toggle).
151
- */
152
- const buildStatusFloater = (): HTMLDivElement => {
153
- const wrap = document.createElement('div')
154
- wrap.className = 'dsh-tw-panel-status'
155
-
156
- const statusBtn = document.createElement('button')
157
- statusBtn.type = 'button'
158
- statusBtn.className = 'dsh-tw-panel-status-btn'
159
- statusBtn.title = '刷新状态'
160
- statusDot = document.createElement('span')
161
- statusDot.className = 'dsh-tw-status-dot'
162
- statusDot.dataset.state = 'unknown'
163
- chip = document.createElement('span')
164
- chip.className = 'dsh-tw-panel-status-text'
165
- chip.textContent = '—'
166
- statusBtn.append(statusDot, chip)
167
- statusBtn.addEventListener('click', () => { void doRefresh() })
168
-
169
- const reloadBtn = document.createElement('button')
170
- reloadBtn.type = 'button'
171
- reloadBtn.className = 'dsh-tw-panel-status-btn'
172
- reloadBtn.textContent = '重载'
173
- reloadBtn.title = '重载 TiddlyWiki 面板'
174
- reloadBtn.addEventListener('click', () => {
175
- if (iframe !== undefined && !iframe.hidden) iframe.src = iframe.src
176
- })
177
-
178
- wrap.append(statusBtn, reloadBtn)
179
- document.body.append(wrap)
180
- return wrap
181
- }
182
-
183
- const setChip = (stateName: string, text: string): void => {
184
- if (statusDot !== undefined) statusDot.dataset.state = stateName
185
- if (chip !== undefined) chip.textContent = text
186
- }
187
-
188
148
  /** Pin the overlay to the center column's current viewport rect. */
189
149
  const syncRect = (): void => {
190
150
  if (container === undefined || columnEl === undefined) return
@@ -274,18 +234,24 @@ export function mountPanel(state: PanelState): () => void {
274
234
  }
275
235
  const payload = await fetchStatus()
276
236
  if (payload === null) {
277
- setChip('failed', '状态不可达')
278
237
  showError('无法访问 /dsh-tiddlywiki/status')
279
238
  return
280
239
  }
281
- if (payload.status === 'running' && typeof payload.url === 'string') {
282
- setChip('running', '在线')
240
+ if (payload.status === 'running') {
283
241
  refreshAttempts = 0
284
- showFrame(payload.url)
242
+ // Same-origin proxy URL: build from the page's own origin so it works no
243
+ // matter which host/domain the user reached DSH on. Fall back to the
244
+ // legacy loopback `url` for older servers that do not send twProxy.
245
+ if (typeof payload.twProxy === 'string') {
246
+ showFrame(new URL(payload.twProxy, window.location.origin).href)
247
+ } else if (typeof payload.url === 'string') {
248
+ showFrame(payload.url)
249
+ } else {
250
+ showError('服务未返回编辑器地址')
251
+ }
285
252
  return
286
253
  }
287
254
  if (payload.status === 'starting') {
288
- setChip('starting', '启动中')
289
255
  showStarting()
290
256
  // Auto-poll while starting (bounded).
291
257
  if (refreshAttempts < 30) {
@@ -294,7 +260,6 @@ export function mountPanel(state: PanelState): () => void {
294
260
  }
295
261
  return
296
262
  }
297
- setChip('failed', '离线')
298
263
  refreshAttempts = 0
299
264
  showError(payload.error ?? `服务状态:${payload.status}`)
300
265
  }
@@ -347,12 +312,12 @@ export function mountPanel(state: PanelState): () => void {
347
312
  ensure()
348
313
  applyActive()
349
314
 
350
- // Bottom-right status/reload buttons, gated by ui.showPanelStatus.
351
- void (async () => {
352
- const payload = await fetchStatus()
353
- if (payload?.ui?.showPanelStatus === false) return
354
- statusFloater = buildStatusFloater()
355
- })()
315
+ // The "知识库" FAB's 重载面板 entry dispatches this event to reload the
316
+ // iframe (the panel itself no longer owns a floating status/reload button).
317
+ const onReloadRequest = (): void => {
318
+ if (iframe !== undefined && !iframe.hidden) iframe.src = iframe.src
319
+ }
320
+ document.addEventListener('dsh-tw-panel-reload', onReloadRequest)
356
321
 
357
322
  return () => {
358
323
  if (refreshTimer !== undefined) window.clearTimeout(refreshTimer)
@@ -362,10 +327,10 @@ export function mountPanel(state: PanelState): () => void {
362
327
  resizeObserver.disconnect()
363
328
  document.removeEventListener('click', onClickSidebarRow, true)
364
329
  document.removeEventListener(ACTIVATE_EVENT, onOtherActivate)
330
+ document.removeEventListener('dsh-tw-panel-reload', onReloadRequest)
365
331
  waitObserver.disconnect()
366
332
  unsubscribe()
367
333
  document.documentElement.removeAttribute(ACTIVE_ATTR)
368
334
  container?.remove()
369
- statusFloater?.remove()
370
335
  }
371
336
  }
@@ -210,9 +210,9 @@ function renderConfigSection(body: HTMLElement, config: Record<string, unknown>,
210
210
  numField('git.debounceMs', '自动 commit 防抖(ms)', typeof git.debounceMs === 'number' ? git.debounceMs : 60_000)
211
211
  textField('git.remote', 'git 远端(空=仅本地)', typeof git.remote === 'string' ? git.remote : '')
212
212
  textField('git.branch', 'git 分支', typeof git.branch === 'string' ? git.branch : 'main')
213
- checkField('ui.showQuickNote', '显示「快速笔记」悬浮按钮', ui.showQuickNote !== false)
214
- checkField('ui.showPanelStatus', '显示 TW 面板右下角「状态/重载」悬浮按钮', ui.showPanelStatus !== false)
215
- checkField('ui.showSyncButton', '显示右下角「同步」悬浮按钮', ui.showSyncButton !== false)
213
+ checkField('ui.showQuickNote', '显示「知识库」按钮里的「快速笔记」入口', ui.showQuickNote !== false)
214
+ checkField('ui.showPanelStatus', '显示「知识库」按钮里的 TW 面板/重载入口与状态行', ui.showPanelStatus !== false)
215
+ checkField('ui.showSyncButton', '显示「知识库」按钮里的「同步」入口与 git 状态点', ui.showSyncButton !== false)
216
216
  // 界面语言在下方「语言管理」区块设置(config 的 uiLanguage 仅供启动时自动应用)。
217
217
 
218
218
  const save = make('button', 'dsh-tw-settings-btn dsh-tw-settings-primary', '保存配置')
@@ -104,6 +104,9 @@ html[data-dsh-tw-active] .dshDesktopConversationSurface > :not([data-dsh-tw-view
104
104
  display: flex; flex-direction: column; align-items: flex-end; gap: 10px;
105
105
  font-family: inherit;
106
106
  }
107
+ /* The wrapper's display:flex would beat the UA [hidden]{display:none} rule,
108
+ so the card's show/hide (root.hidden) needs an explicit rule. */
109
+ .dsh-tw-note[hidden] { display: none; }
107
110
  .dsh-tw-note-card {
108
111
  width: 340px; max-width: calc(100vw - 40px);
109
112
  background: var(--dsw-alias-bg-layer-2, #fff); color: var(--dsw-alias-label-primary, #222);
@@ -344,6 +347,113 @@ html[data-dsh-tw-active] .dshDesktopConversationSurface > :not([data-dsh-tw-view
344
347
  .dsh-tw-settings-error { color: var(--dsw-alias-state-error-primary, #d13b3b); font-size: 12px; }
345
348
  .dsh-tw-settings-search { flex: 0 0 auto; max-width: 220px; }
346
349
  .dsh-tw-settings-check { accent-color: var(--dsw-alias-brand-primary, #3e63dd); }
350
+
351
+ /* ── "知识库" FAB (v0.5: quick-note + sync + panel status merged) ──────────
352
+ One fixed cluster above the shutdown launcher FAB (bottom:24). The FAB
353
+ carries a git status dot; the menu pops upward from it. */
354
+ .dsh-tw-fab-wrap {
355
+ position: fixed; right: 24px; bottom: 88px; z-index: 960;
356
+ display: flex; flex-direction: column; align-items: flex-end; gap: 8px;
357
+ }
358
+ .dsh-tw-fab {
359
+ position: relative;
360
+ width: 46px; height: 46px; border-radius: 50%;
361
+ border: 1px solid var(--dsw-alias-border-l2, rgba(0,0,0,.18));
362
+ background: var(--dsw-alias-bg-layer-2, #fff); color: var(--dsw-alias-label-primary, #222);
363
+ box-shadow: var(--dsw-shadow-lv3, 0 4px 16px rgba(0,0,0,.16)); cursor: pointer;
364
+ display: inline-flex; align-items: center; justify-content: center;
365
+ transition: background-color 120ms ease, transform 80ms ease;
366
+ }
367
+ .dsh-tw-fab:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,.1)); }
368
+ .dsh-tw-fab:active { transform: scale(.94); }
369
+ .dsh-tw-fab-icon { display: inline-flex; }
370
+ .dsh-tw-fab-dot {
371
+ position: absolute; right: 0; bottom: 0; width: 11px; height: 11px;
372
+ border-radius: 50%; border: 2px solid var(--dsw-alias-bg-layer-2, #fff);
373
+ background: #999; box-sizing: border-box;
374
+ }
375
+ .dsh-tw-fab-dot[data-state="clean"] { background: var(--dsw-alias-state-success-primary, #3eaa5f); }
376
+ .dsh-tw-fab-dot[data-state="dirty"] { background: var(--dsw-alias-state-warning-primary, #d9822b); }
377
+ .dsh-tw-fab-dot[data-state="behind"] { background: var(--dsw-alias-state-error-primary, #d13b3b); }
378
+ .dsh-tw-fab-dot[data-state="syncing"] { background: var(--dsw-alias-brand-primary, #3e63dd); }
379
+ .dsh-tw-fab-menu {
380
+ min-width: 220px; max-width: calc(100vw - 48px);
381
+ display: flex; flex-direction: column; gap: 2px; padding: 6px;
382
+ border-radius: 12px; border: 1px solid var(--dsw-alias-border-l2, rgba(0,0,0,.15));
383
+ background: var(--dsw-alias-bg-layer-2, #fff); color: var(--dsw-alias-label-primary, #222);
384
+ box-shadow: var(--dsw-shadow-lv3, 0 8px 30px rgba(0,0,0,.22));
385
+ font-size: 13px;
386
+ backdrop-filter: blur(14px); -webkit-backdrop-filter: blur(14px);
387
+ animation: dsh-tw-note-in 160ms ease;
388
+ }
389
+ .dsh-tw-fab-menu[hidden] { display: none; }
390
+ .dsh-tw-fab-status {
391
+ display: flex; align-items: center; gap: 7px;
392
+ padding: 6px 9px; font-size: 12px; color: var(--dsw-alias-label-secondary, #666);
393
+ border-bottom: 1px solid var(--dsw-alias-border-l1, rgba(0,0,0,.08));
394
+ margin-bottom: 4px;
395
+ }
396
+ .dsh-tw-fab-status + .dsh-tw-fab-status { border-bottom: 0; margin-bottom: 4px; }
397
+ .dsh-tw-fab-status-text { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
398
+ .dsh-tw-fab-status-dot { width: 8px; height: 8px; border-radius: 50%; background: #999; flex: none; }
399
+ .dsh-tw-fab-status-dot[data-state="running"] { background: var(--dsw-alias-state-success-primary, #3eaa5f); }
400
+ .dsh-tw-fab-status-dot[data-state="starting"] { background: var(--dsw-alias-state-warning-primary, #d9822b); }
401
+ .dsh-tw-fab-status-dot[data-state="failed"], .dsh-tw-fab-status-dot[data-state="stopped"] { background: var(--dsw-alias-state-error-primary, #d13b3b); }
402
+ .dsh-tw-fab-status-dot[data-state="clean"] { background: var(--dsw-alias-state-success-primary, #3eaa5f); }
403
+ .dsh-tw-fab-status-dot[data-state="dirty"] { background: var(--dsw-alias-state-warning-primary, #d9822b); }
404
+ .dsh-tw-fab-status-dot[data-state="behind"] { background: var(--dsw-alias-state-error-primary, #d13b3b); }
405
+ .dsh-tw-fab-status-dot[data-state="syncing"] { background: var(--dsw-alias-brand-primary, #3e63dd); }
406
+ .dsh-tw-fab-item {
407
+ display: flex; align-items: center; gap: 6px; text-align: left;
408
+ border: none; background: transparent; color: inherit; font: inherit; font-size: 13px;
409
+ padding: 7px 9px; border-radius: 8px; cursor: pointer;
410
+ }
411
+ .dsh-tw-fab-item:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,.12)); }
412
+ .dsh-tw-fab-item:active { background: var(--dsw-alias-interactive-bg-active, rgba(128,128,128,.18)); }
413
+
414
+ /* ── quick-note restored-draft banner ─────────────────────────────────────── */
415
+ .dsh-tw-note-draft {
416
+ display: flex; align-items: center; justify-content: space-between; gap: 8px;
417
+ padding: 5px 9px; border-radius: 8px; font-size: 12px;
418
+ background: color-mix(in srgb, var(--dsw-alias-state-warning-primary, #d9822b) 12%, transparent);
419
+ color: var(--dsw-alias-label-primary, #222);
420
+ }
421
+ .dsh-tw-note-draft[hidden] { display: none; }
422
+ .dsh-tw-note-draft-text { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
423
+ .dsh-tw-note-draft-discard {
424
+ border: 1px solid var(--dsw-alias-border-l2, rgba(0,0,0,.18));
425
+ background: transparent; color: inherit; font: inherit; font-size: 12px;
426
+ padding: 2px 10px; border-radius: 999px; cursor: pointer; flex: none;
427
+ }
428
+ .dsh-tw-note-draft-discard:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,.12)); }
429
+
430
+ /* ── quick-note recent-notes picker ───────────────────────────────────────── */
431
+ .dsh-tw-note-recent {
432
+ position: absolute; right: 0; bottom: calc(100% - 8px); z-index: 70;
433
+ width: 340px; max-width: calc(100vw - 40px); max-height: 280px; overflow-y: auto;
434
+ display: flex; flex-direction: column; gap: 2px; padding: 6px;
435
+ border-radius: 12px; border: 1px solid var(--dsw-alias-border-l2, rgba(0,0,0,.15));
436
+ background: var(--dsw-alias-bg-layer-2, #fff); color: var(--dsw-alias-label-primary, #222);
437
+ box-shadow: var(--dsw-shadow-lv3, 0 8px 30px rgba(0,0,0,.22));
438
+ font-size: 13px;
439
+ }
440
+ .dsh-tw-note-recent[hidden] { display: none; }
441
+ .dsh-tw-note-recent-item {
442
+ display: flex; align-items: baseline; justify-content: space-between; gap: 10px;
443
+ padding: 6px 8px; border-radius: 7px; cursor: pointer;
444
+ }
445
+ .dsh-tw-note-recent-item:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,.12)); }
446
+ .dsh-tw-note-recent-name { font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
447
+ .dsh-tw-note-recent-meta { flex: none; font-size: 11px; color: var(--dsw-alias-label-dimmed, #999); }
448
+ .dsh-tw-note-recent-muted { padding: 8px 10px; font-size: 12px; color: var(--dsw-alias-label-dimmed, #999); }
449
+ .dsh-tw-note-recent-btn {
450
+ border: 1px solid var(--dsw-alias-border-l2, rgba(0,0,0,.18));
451
+ background: var(--dsw-alias-bg-layer-2, #fff); color: var(--dsw-alias-label-primary, #222);
452
+ font: inherit; font-size: 12px; padding: 6px 10px; border-radius: 8px; cursor: pointer;
453
+ transition: filter 120ms ease, transform 80ms ease;
454
+ }
455
+ .dsh-tw-note-recent-btn:hover { filter: brightness(.96); }
456
+ .dsh-tw-note-recent-btn:active { transform: scale(.96); }
347
457
  `
348
458
 
349
459
  export function injectStyles(): void {