dsh-tiddlywiki 0.1.2 → 0.2.1
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/README.md +7 -4
- package/lib/client.bundle.js +276 -67
- package/lib/client.js +276 -67
- package/lib/index.js +2225 -2152
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
- package/src/client/note-widget.ts +373 -210
- package/src/client/panel.ts +51 -25
- package/src/client/settings-page.ts +3 -0
- package/src/client/styles.ts +68 -25
- package/src/host/config.ts +6 -0
- package/src/host/routes.ts +49 -10
- package/src/host/seed-notes.ts +79 -62
- package/src/index.ts +16 -4
package/README.md
CHANGED
|
@@ -24,7 +24,7 @@ TiddlyWiki 的特性正好合适:单文件、纯文本、wiki 语法、自带
|
|
|
24
24
|
|---|---|
|
|
25
25
|
| Agent 工具 | `tiddlywiki_search` / `get` / `put` / `delete` / `git_sync` |
|
|
26
26
|
| 人编辑 | 侧边栏「TiddlyWiki」入口 → 中央列内嵌完整 TW 编辑器(iframe 直连 TW 服务) |
|
|
27
|
-
| 快速笔记 |
|
|
27
|
+
| 快速笔记 | 聊天区右下角常驻悬浮控件(可折叠,可多选/自动补全 tag,Ctrl+Enter 保存;设置页可整体隐藏) |
|
|
28
28
|
| 零摩擦生命周期 | 随 dsh 自动启动/关闭;TW 子进程崩溃自动重启(退避);端口/目录/首次 git init 全自动 |
|
|
29
29
|
| 同步 | 单线程交替模型:开工 pull,收工 commit+push,自动 commit(60s 防抖,可关) |
|
|
30
30
|
|
|
@@ -43,13 +43,13 @@ dsh plugin --profile web add github:bbqisbbq/dsh-tiddlywiki
|
|
|
43
43
|
dsh plugin --profile web add link:C:\Users\bbq\.dsh\plugins\dsh-tiddlywiki
|
|
44
44
|
```
|
|
45
45
|
|
|
46
|
-
首次启动会向 wiki
|
|
46
|
+
首次启动会向 wiki **写入一次**「dsh-tiddlywiki 插件说明」笔记(tag `docs`)。
|
|
47
47
|
|
|
48
48
|
## 发布 / 仓库
|
|
49
49
|
|
|
50
50
|
- **GitHub(公开)**:https://github.com/bbqisbbq/dsh-tiddlywiki
|
|
51
51
|
- **npm**:`dsh-tiddlywiki`(`npm i dsh-tiddlywiki`;https://www.npmjs.com/package/dsh-tiddlywiki )
|
|
52
|
-
-
|
|
52
|
+
- **说明笔记**:插件在**首次启动**时向 wiki **写入一次**「`dsh-tiddlywiki 插件说明`」笔记(tag `docs`)——只写一次、手动编辑不被覆盖、**删除后重启不会自动恢复**(一次性标记;清空 wiki 重装会再写入)。当前 live wiki 里已有一份。
|
|
53
53
|
|
|
54
54
|
## 可被检索的标准字段
|
|
55
55
|
|
|
@@ -85,6 +85,9 @@ dsh plugin --profile web add link:C:\Users\bbq\.dsh\plugins\dsh-tiddlywiki
|
|
|
85
85
|
branch: "main"
|
|
86
86
|
note:
|
|
87
87
|
tag: "inbox"
|
|
88
|
+
ui:
|
|
89
|
+
showQuickNote: true # 是否显示右下角「快速笔记」按钮
|
|
90
|
+
showPanelStatus: true # 是否显示 TW 面板右下角「状态/重载」悬浮按钮
|
|
88
91
|
auth:
|
|
89
92
|
username: "" # 默认 loopback 匿名;暴露到非 loopback 时才需要
|
|
90
93
|
password: ""
|
|
@@ -95,7 +98,7 @@ dsh plugin --profile web add link:C:\Users\bbq\.dsh\plugins\dsh-tiddlywiki
|
|
|
95
98
|
|
|
96
99
|
> 运行时配置:设置页(见「设置页」)写入的 `$:/plugins/dsh-tiddlywiki/config`
|
|
97
100
|
> tiddler 是 `config:` 块之上的覆盖层(tiddler 优先、随 wiki 的 git 同步)。
|
|
98
|
-
> 无需改动 cordis 也能改 note tag / git 开关等。
|
|
101
|
+
> 无需改动 cordis 也能改 note tag / git 开关 / ui 开关等。
|
|
99
102
|
|
|
100
103
|
## 使用
|
|
101
104
|
|
package/lib/client.bundle.js
CHANGED
|
@@ -48,6 +48,21 @@ const CSS_TEXT = `
|
|
|
48
48
|
.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; }
|
|
49
49
|
.dsh-tw-entry svg { flex: none; }
|
|
50
50
|
.dsh-tw-entry .dsh-tw-entry-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
51
|
+
/* Collapsed rail: the shell narrows the sidebar to a 56px icon rail (dual
|
|
52
|
+
signals — the frame's data-sidebar-collapsed + the sidebar root's hashed
|
|
53
|
+
*_collapsed class, same doctrine as dsh-taskboard 0.4.3). Mirror the
|
|
54
|
+
native 36×36 icon-button geometry: center the icon, hide the label, and
|
|
55
|
+
scale it up to match the shell's 18px rail icons. */
|
|
56
|
+
[data-sidebar-collapsed] [data-dsh-tw-entry],
|
|
57
|
+
[class*="_collapsed"] [data-dsh-tw-entry] {
|
|
58
|
+
width: 36px; height: 36px; min-width: 36px;
|
|
59
|
+
margin: 0 0 12px; padding: 0;
|
|
60
|
+
justify-content: center; gap: 0; text-align: center;
|
|
61
|
+
}
|
|
62
|
+
[data-sidebar-collapsed] [data-dsh-tw-entry] .dsh-tw-entry-label,
|
|
63
|
+
[class*="_collapsed"] [data-dsh-tw-entry] .dsh-tw-entry-label { display: none; }
|
|
64
|
+
[data-sidebar-collapsed] [data-dsh-tw-entry] svg,
|
|
65
|
+
[class*="_collapsed"] [data-dsh-tw-entry] svg { width: 18px; height: 18px; }
|
|
51
66
|
|
|
52
67
|
/* ── center-column panel (fixed overlay, JS-pinned to the column rect) ── */
|
|
53
68
|
.dsh-tw-view {
|
|
@@ -60,31 +75,31 @@ html[data-dsh-tw-active] [data-pane="conversation"] > :not([data-dsh-tw-view]),
|
|
|
60
75
|
html[data-dsh-tw-active] [class*="centerCol"] > :not([data-dsh-tw-view]),
|
|
61
76
|
html[data-dsh-tw-active] .dshDesktopConversationSurface > :not([data-dsh-tw-view]) { display: none !important; }
|
|
62
77
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
78
|
+
/* ── bottom-right status / reload floating buttons ───────────────────
|
|
79
|
+
The top panel bar (title + status + reload row) is gone so TW's menubar
|
|
80
|
+
reaches the panel top; status/reload now live here as floating buttons,
|
|
81
|
+
same visual language as the quick-note toggle. The ui.showPanelStatus
|
|
82
|
+
setting controls whether they mount at all. */
|
|
83
|
+
.dsh-tw-panel-status {
|
|
84
|
+
/* 状态/重载悬浮按钮:右对齐到与快速笔记按钮同一条竖线(right:24),
|
|
85
|
+
垂直排在快速笔记按钮(bottom:88,高约 38px)上方;
|
|
86
|
+
两个按钮竖直依次排列(状态在上、重载在下),与其他插件按钮列对齐、不堆叠。 */
|
|
87
|
+
position: fixed; right: 24px; bottom: 140px; z-index: 950;
|
|
88
|
+
display: flex; flex-direction: column; align-items: flex-end; gap: 8px;
|
|
69
89
|
}
|
|
70
|
-
.dsh-tw-panel-
|
|
71
|
-
border: 1px solid var(--dsw-alias-border-l2, rgba(
|
|
72
|
-
background:
|
|
73
|
-
padding:
|
|
90
|
+
.dsh-tw-panel-status-btn {
|
|
91
|
+
border: 1px solid var(--dsw-alias-border-l2, rgba(0,0,0,.18));
|
|
92
|
+
background: var(--dsw-alias-bg-layer-2, #fff); color: var(--dsw-alias-label-primary, #222);
|
|
93
|
+
font: inherit; font-size: 12px; padding: 7px 12px; border-radius: 999px;
|
|
94
|
+
box-shadow: var(--dsw-shadow-lv3, 0 4px 16px rgba(0,0,0,.16)); cursor: pointer;
|
|
95
|
+
display: inline-flex; align-items: center; gap: 6px;
|
|
96
|
+
transition: background-color 120ms ease;
|
|
74
97
|
}
|
|
75
|
-
.dsh-tw-panel-
|
|
76
|
-
.dsh-tw-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
cluster's width at the panel bar's right end — mirroring how
|
|
81
|
-
dsh-better-sidebar pads the DSH session header — and the panel's own
|
|
82
|
-
状态/重载 buttons never sit underneath it. */
|
|
83
|
-
.dsh-tw-view[data-sidebar-host] .dsh-tw-panel-bar { padding-right: 80px; }
|
|
84
|
-
.dsh-tw-status-chip { font-variant-numeric: tabular-nums; }
|
|
85
|
-
.dsh-tw-status-chip[data-state="running"] { color: var(--dsw-alias-state-success-primary, #3eaa5f); }
|
|
86
|
-
.dsh-tw-status-chip[data-state="starting"] { color: var(--dsw-alias-state-warning-primary, #d9822b); }
|
|
87
|
-
.dsh-tw-status-chip[data-state="failed"], .dsh-tw-status-chip[data-state="stopped"] { color: var(--dsw-alias-state-error-primary, #d13b3b); }
|
|
98
|
+
.dsh-tw-panel-status-btn:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,.1)); }
|
|
99
|
+
.dsh-tw-status-dot { width: 8px; height: 8px; border-radius: 50%; background: #999; flex: none; }
|
|
100
|
+
.dsh-tw-status-dot[data-state="running"] { background: var(--dsw-alias-state-success-primary, #3eaa5f); }
|
|
101
|
+
.dsh-tw-status-dot[data-state="starting"] { background: var(--dsw-alias-state-warning-primary, #d9822b); }
|
|
102
|
+
.dsh-tw-status-dot[data-state="failed"], .dsh-tw-status-dot[data-state="stopped"] { background: var(--dsw-alias-state-error-primary, #d13b3b); }
|
|
88
103
|
|
|
89
104
|
.dsh-tw-panel-frame { flex: 1; min-height: 0; border: 0; width: 100%; display: block; background: #fff; }
|
|
90
105
|
/* [hidden] must beat the author display rules above (UA hidden is overridden). */
|
|
@@ -163,6 +178,34 @@ html[data-dsh-tw-active] .dshDesktopConversationSurface > :not([data-dsh-tw-view
|
|
|
163
178
|
}
|
|
164
179
|
.dsh-tw-note-edit:hover:not(:disabled) { filter: brightness(.96); }
|
|
165
180
|
.dsh-tw-note-edit:disabled { opacity: .55; cursor: default; }
|
|
181
|
+
/* ── Multi-tag chip editor (quick-note tags) ─────────────────────────────── */
|
|
182
|
+
.dsh-tw-note-tags { position: relative; flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 4px; }
|
|
183
|
+
.dsh-tw-note-chips { display: flex; flex-wrap: wrap; gap: 4px; }
|
|
184
|
+
.dsh-tw-note-tagchip {
|
|
185
|
+
display: inline-flex; align-items: center; gap: 4px;
|
|
186
|
+
padding: 2px 6px 2px 8px; border-radius: 999px; font-size: 11px; line-height: 1.4;
|
|
187
|
+
background: color-mix(in srgb, var(--dsw-alias-brand-primary, #3e63dd) 10%, var(--dsw-alias-bg-layer-1, #fff));
|
|
188
|
+
border: 1px solid color-mix(in srgb, var(--dsw-alias-brand-primary, #3e63dd) 30%, transparent);
|
|
189
|
+
color: var(--dsw-alias-label-primary, #222);
|
|
190
|
+
}
|
|
191
|
+
.dsh-tw-note-tagchip-x {
|
|
192
|
+
cursor: pointer; font-size: 12px; line-height: 1; padding: 0 2px; border-radius: 50%;
|
|
193
|
+
color: var(--dsw-alias-label-secondary, #888);
|
|
194
|
+
}
|
|
195
|
+
.dsh-tw-note-tagchip-x:hover { color: var(--dsw-alias-state-error-primary, #d13b3b); }
|
|
196
|
+
.dsh-tw-note-taginput { width: 100%; box-sizing: border-box; }
|
|
197
|
+
.dsh-tw-note-tagsuggest {
|
|
198
|
+
position: absolute; left: 0; right: 0; top: calc(100% + 2px); z-index: 60;
|
|
199
|
+
max-height: 168px; overflow-y: auto; border-radius: 8px; padding: 4px;
|
|
200
|
+
background: var(--dsw-alias-bg-layer-2, #fff); color: var(--dsw-alias-label-primary, #222);
|
|
201
|
+
border: 1px solid var(--dsw-alias-border-l2, rgba(0,0,0,.18));
|
|
202
|
+
box-shadow: var(--dsw-shadow-lv2, 0 4px 16px rgba(0,0,0,.16));
|
|
203
|
+
font-size: 12px;
|
|
204
|
+
}
|
|
205
|
+
.dsh-tw-note-tagsuggest-item {
|
|
206
|
+
padding: 6px 9px; border-radius: 6px; cursor: pointer; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
|
207
|
+
}
|
|
208
|
+
.dsh-tw-note-tagsuggest-item:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,.12)); }
|
|
166
209
|
/* ── Native-editor popup iframe (quick-note "在 TW 中编辑") ──────────────── */
|
|
167
210
|
.dsh-tw-editor-popup {
|
|
168
211
|
position: fixed; left: 0; right: 0; top: 0; bottom: 0; margin: auto;
|
|
@@ -497,34 +540,14 @@ function mountPanel(state) {
|
|
|
497
540
|
let frameArea;
|
|
498
541
|
let errorArea;
|
|
499
542
|
let chip;
|
|
543
|
+
let statusDot;
|
|
544
|
+
let statusFloater;
|
|
500
545
|
let refreshTimer;
|
|
501
546
|
let refreshAttempts = 0;
|
|
502
547
|
const build = () => {
|
|
503
548
|
const view = document.createElement("div");
|
|
504
549
|
view.dataset.dshTwView = "";
|
|
505
550
|
view.className = "dsh-tw-view";
|
|
506
|
-
const bar = document.createElement("div");
|
|
507
|
-
bar.className = "dsh-tw-panel-bar";
|
|
508
|
-
const title = document.createElement("span");
|
|
509
|
-
title.className = "dsh-tw-panel-title";
|
|
510
|
-
title.textContent = "TiddlyWiki 知识库";
|
|
511
|
-
chip = document.createElement("span");
|
|
512
|
-
chip.className = "dsh-tw-status-chip";
|
|
513
|
-
chip.dataset.state = "unknown";
|
|
514
|
-
chip.textContent = "—";
|
|
515
|
-
const reload = document.createElement("button");
|
|
516
|
-
reload.type = "button";
|
|
517
|
-
reload.textContent = "重载";
|
|
518
|
-
reload.addEventListener("click", () => {
|
|
519
|
-
if (iframe !== void 0 && !iframe.hidden) iframe.src = iframe.src;
|
|
520
|
-
});
|
|
521
|
-
const refresh = document.createElement("button");
|
|
522
|
-
refresh.type = "button";
|
|
523
|
-
refresh.textContent = "状态";
|
|
524
|
-
refresh.addEventListener("click", () => {
|
|
525
|
-
doRefresh();
|
|
526
|
-
});
|
|
527
|
-
bar.append(title, chip, refresh, reload);
|
|
528
551
|
frameArea = document.createElement("div");
|
|
529
552
|
frameArea.className = "dsh-tw-panel-frame-wrap";
|
|
530
553
|
frameArea.style.cssText = "flex:1;min-height:0;display:flex;flex-direction:column";
|
|
@@ -536,13 +559,47 @@ function mountPanel(state) {
|
|
|
536
559
|
errorArea = document.createElement("div");
|
|
537
560
|
errorArea.className = "dsh-tw-panel-error";
|
|
538
561
|
errorArea.hidden = true;
|
|
539
|
-
view.append(
|
|
562
|
+
view.append(frameArea, errorArea);
|
|
540
563
|
return view;
|
|
541
564
|
};
|
|
565
|
+
/**
|
|
566
|
+
* Bottom-right floating status/reload buttons (same visual language as the
|
|
567
|
+
* quick-note toggle) — the top panel bar is gone, so TW's menubar reaches
|
|
568
|
+
* the very top of the panel and clears the host's top-right buttons. Mount
|
|
569
|
+
* is gated by `ui.showPanelStatus` (settings page toggle).
|
|
570
|
+
*/
|
|
571
|
+
const buildStatusFloater = () => {
|
|
572
|
+
const wrap = document.createElement("div");
|
|
573
|
+
wrap.className = "dsh-tw-panel-status";
|
|
574
|
+
const statusBtn = document.createElement("button");
|
|
575
|
+
statusBtn.type = "button";
|
|
576
|
+
statusBtn.className = "dsh-tw-panel-status-btn";
|
|
577
|
+
statusBtn.title = "刷新状态";
|
|
578
|
+
statusDot = document.createElement("span");
|
|
579
|
+
statusDot.className = "dsh-tw-status-dot";
|
|
580
|
+
statusDot.dataset.state = "unknown";
|
|
581
|
+
chip = document.createElement("span");
|
|
582
|
+
chip.className = "dsh-tw-panel-status-text";
|
|
583
|
+
chip.textContent = "—";
|
|
584
|
+
statusBtn.append(statusDot, chip);
|
|
585
|
+
statusBtn.addEventListener("click", () => {
|
|
586
|
+
doRefresh();
|
|
587
|
+
});
|
|
588
|
+
const reloadBtn = document.createElement("button");
|
|
589
|
+
reloadBtn.type = "button";
|
|
590
|
+
reloadBtn.className = "dsh-tw-panel-status-btn";
|
|
591
|
+
reloadBtn.textContent = "重载";
|
|
592
|
+
reloadBtn.title = "重载 TiddlyWiki 面板";
|
|
593
|
+
reloadBtn.addEventListener("click", () => {
|
|
594
|
+
if (iframe !== void 0 && !iframe.hidden) iframe.src = iframe.src;
|
|
595
|
+
});
|
|
596
|
+
wrap.append(statusBtn, reloadBtn);
|
|
597
|
+
document.body.append(wrap);
|
|
598
|
+
return wrap;
|
|
599
|
+
};
|
|
542
600
|
const setChip = (stateName, text) => {
|
|
543
|
-
if (
|
|
544
|
-
chip.
|
|
545
|
-
chip.textContent = text;
|
|
601
|
+
if (statusDot !== void 0) statusDot.dataset.state = stateName;
|
|
602
|
+
if (chip !== void 0) chip.textContent = text;
|
|
546
603
|
};
|
|
547
604
|
/** Pin the overlay to the center column's current viewport rect. */
|
|
548
605
|
const syncRect = () => {
|
|
@@ -696,6 +753,10 @@ function mountPanel(state) {
|
|
|
696
753
|
const unsubscribe = state.subscribe(applyActive);
|
|
697
754
|
ensure();
|
|
698
755
|
applyActive();
|
|
756
|
+
(async () => {
|
|
757
|
+
if ((await fetchStatus())?.ui?.showPanelStatus === false) return;
|
|
758
|
+
statusFloater = buildStatusFloater();
|
|
759
|
+
})();
|
|
699
760
|
return () => {
|
|
700
761
|
if (refreshTimer !== void 0) window.clearTimeout(refreshTimer);
|
|
701
762
|
window.clearInterval(syncInterval);
|
|
@@ -708,6 +769,7 @@ function mountPanel(state) {
|
|
|
708
769
|
unsubscribe();
|
|
709
770
|
document.documentElement.removeAttribute(ACTIVE_ATTR);
|
|
710
771
|
container?.remove();
|
|
772
|
+
statusFloater?.remove();
|
|
711
773
|
};
|
|
712
774
|
}
|
|
713
775
|
//#endregion
|
|
@@ -847,14 +909,23 @@ function ensurePopup() {
|
|
|
847
909
|
* collapsible, independent of any shell DOM. Lets the human jot drafts /
|
|
848
910
|
* scratch notes while waiting for the AI or drafting the next prompt.
|
|
849
911
|
*
|
|
850
|
-
* Save posts to /dsh-tiddlywiki/note → an independent tiddler (title &
|
|
912
|
+
* Save posts to /dsh-tiddlywiki/note → an independent tiddler (title & tags
|
|
851
913
|
* editable; defaults: timestamp title + config tag, usually "inbox").
|
|
852
914
|
*
|
|
915
|
+
* ui.showQuickNote config: the widget is mounted async and stays hidden (and
|
|
916
|
+
* never appended) when the option is off, so the toggle button can be disabled
|
|
917
|
+
* from the settings page without touching the shell.
|
|
918
|
+
*
|
|
919
|
+
* Tag editor: multi-select chips + autocomplete from the wiki's existing tags
|
|
920
|
+
* (GET /dsh-tiddlywiki/tags). Enter/`,` commits the typed value; Backspace on
|
|
921
|
+
* an empty field removes the last chip; the dropdown filters on typing.
|
|
922
|
+
*
|
|
853
923
|
* @module dsh-tiddlywiki/client/note-widget
|
|
854
924
|
*/
|
|
855
925
|
const NOTE_ENDPOINT = "/dsh-tiddlywiki/note";
|
|
856
926
|
const EDIT_ENDPOINT = "/dsh-tiddlywiki/edit";
|
|
857
927
|
const STATUS_ENDPOINT = "/dsh-tiddlywiki/status";
|
|
928
|
+
const TAGS_ENDPOINT = "/dsh-tiddlywiki/tags";
|
|
858
929
|
function pad(n) {
|
|
859
930
|
return n < 10 ? `0${n}` : String(n);
|
|
860
931
|
}
|
|
@@ -862,16 +933,137 @@ function pad(n) {
|
|
|
862
933
|
function timestampTitle(date = /* @__PURE__ */ new Date()) {
|
|
863
934
|
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}`;
|
|
864
935
|
}
|
|
865
|
-
async function
|
|
936
|
+
async function fetchUiOptions() {
|
|
937
|
+
const fallback = {
|
|
938
|
+
showQuickNote: true,
|
|
939
|
+
defaultTag: "inbox"
|
|
940
|
+
};
|
|
866
941
|
try {
|
|
867
942
|
const res = await fetch(STATUS_ENDPOINT, { signal: AbortSignal.timeout(5e3) });
|
|
868
|
-
if (!res.ok) return
|
|
869
|
-
|
|
943
|
+
if (!res.ok) return fallback;
|
|
944
|
+
const payload = await res.json();
|
|
945
|
+
return {
|
|
946
|
+
showQuickNote: payload.ui?.showQuickNote !== false,
|
|
947
|
+
defaultTag: payload.note?.tag ?? "inbox"
|
|
948
|
+
};
|
|
870
949
|
} catch {
|
|
871
|
-
return
|
|
950
|
+
return fallback;
|
|
872
951
|
}
|
|
873
952
|
}
|
|
874
|
-
|
|
953
|
+
/** Multi-tag chip editor with autocomplete from the wiki's existing tags. */
|
|
954
|
+
function buildTagEditor() {
|
|
955
|
+
const wrap = document.createElement("div");
|
|
956
|
+
wrap.className = "dsh-tw-note-tags";
|
|
957
|
+
const chipWrap = document.createElement("div");
|
|
958
|
+
chipWrap.className = "dsh-tw-note-chips";
|
|
959
|
+
const input = document.createElement("input");
|
|
960
|
+
input.className = "dsh-tw-note-taginput";
|
|
961
|
+
input.placeholder = "tag(可多选,自动补全)";
|
|
962
|
+
const suggest = document.createElement("div");
|
|
963
|
+
suggest.className = "dsh-tw-note-tagsuggest";
|
|
964
|
+
suggest.hidden = true;
|
|
965
|
+
wrap.append(chipWrap, input, suggest);
|
|
966
|
+
const chips = [];
|
|
967
|
+
const hideSuggest = () => {
|
|
968
|
+
suggest.hidden = true;
|
|
969
|
+
};
|
|
970
|
+
const renderChips = () => {
|
|
971
|
+
chipWrap.replaceChildren();
|
|
972
|
+
for (const tag of chips) {
|
|
973
|
+
const chip = document.createElement("span");
|
|
974
|
+
chip.className = "dsh-tw-note-tagchip";
|
|
975
|
+
chip.textContent = tag;
|
|
976
|
+
const x = document.createElement("span");
|
|
977
|
+
x.className = "dsh-tw-note-tagchip-x";
|
|
978
|
+
x.textContent = "×";
|
|
979
|
+
x.title = `移除 tag「${tag}」`;
|
|
980
|
+
x.addEventListener("click", (event) => {
|
|
981
|
+
event.stopPropagation();
|
|
982
|
+
const i = chips.indexOf(tag);
|
|
983
|
+
if (i >= 0) {
|
|
984
|
+
chips.splice(i, 1);
|
|
985
|
+
renderChips();
|
|
986
|
+
}
|
|
987
|
+
});
|
|
988
|
+
chip.append(x);
|
|
989
|
+
chipWrap.append(chip);
|
|
990
|
+
}
|
|
991
|
+
};
|
|
992
|
+
const addTag = (tag) => {
|
|
993
|
+
const t = tag.trim();
|
|
994
|
+
if (t.length === 0 || chips.includes(t)) return;
|
|
995
|
+
chips.push(t);
|
|
996
|
+
input.value = "";
|
|
997
|
+
renderChips();
|
|
998
|
+
hideSuggest();
|
|
999
|
+
input.focus();
|
|
1000
|
+
};
|
|
1001
|
+
const commitInput = () => {
|
|
1002
|
+
for (const raw of input.value.split(/\s+/)) addTag(raw);
|
|
1003
|
+
};
|
|
1004
|
+
let knownTags = [];
|
|
1005
|
+
let tagsPromise;
|
|
1006
|
+
const ensureTags = () => {
|
|
1007
|
+
tagsPromise ??= fetch(TAGS_ENDPOINT, { signal: AbortSignal.timeout(5e3) }).then((r) => r.ok ? r.json() : Promise.resolve({})).then((p) => [...p.tags ?? []].sort((a, b) => a.localeCompare(b, "zh"))).catch(() => []);
|
|
1008
|
+
tagsPromise.then((list) => {
|
|
1009
|
+
knownTags = list;
|
|
1010
|
+
});
|
|
1011
|
+
return tagsPromise;
|
|
1012
|
+
};
|
|
1013
|
+
const showSuggest = () => {
|
|
1014
|
+
const q = input.value.trim().toLowerCase();
|
|
1015
|
+
const matches = knownTags.filter((t) => !chips.includes(t) && (q.length === 0 || t.toLowerCase().includes(q))).slice(0, 8);
|
|
1016
|
+
suggest.replaceChildren();
|
|
1017
|
+
for (const tag of matches) {
|
|
1018
|
+
const item = document.createElement("div");
|
|
1019
|
+
item.className = "dsh-tw-note-tagsuggest-item";
|
|
1020
|
+
item.textContent = tag;
|
|
1021
|
+
item.addEventListener("mousedown", (event) => {
|
|
1022
|
+
event.preventDefault();
|
|
1023
|
+
addTag(tag);
|
|
1024
|
+
});
|
|
1025
|
+
suggest.append(item);
|
|
1026
|
+
}
|
|
1027
|
+
suggest.hidden = matches.length === 0;
|
|
1028
|
+
};
|
|
1029
|
+
input.addEventListener("focus", () => {
|
|
1030
|
+
ensureTags().then(showSuggest);
|
|
1031
|
+
});
|
|
1032
|
+
input.addEventListener("input", () => {
|
|
1033
|
+
if (knownTags.length === 0) ensureTags().then(showSuggest);
|
|
1034
|
+
else showSuggest();
|
|
1035
|
+
});
|
|
1036
|
+
input.addEventListener("keydown", (event) => {
|
|
1037
|
+
if (event.key === "Enter" || event.key === ",") {
|
|
1038
|
+
event.preventDefault();
|
|
1039
|
+
commitInput();
|
|
1040
|
+
} else if (event.key === "Backspace" && input.value.length === 0 && chips.length > 0) {
|
|
1041
|
+
chips.pop();
|
|
1042
|
+
renderChips();
|
|
1043
|
+
} else if (event.key === "Escape") hideSuggest();
|
|
1044
|
+
});
|
|
1045
|
+
document.addEventListener("click", (event) => {
|
|
1046
|
+
if (!wrap.contains(event.target)) hideSuggest();
|
|
1047
|
+
}, true);
|
|
1048
|
+
return {
|
|
1049
|
+
el: wrap,
|
|
1050
|
+
getTags: () => {
|
|
1051
|
+
commitInput();
|
|
1052
|
+
return [...chips];
|
|
1053
|
+
},
|
|
1054
|
+
setDefault: (tag) => {
|
|
1055
|
+
if (chips.length === 0) {
|
|
1056
|
+
const t = tag.trim();
|
|
1057
|
+
if (t.length > 0) {
|
|
1058
|
+
chips.push(t);
|
|
1059
|
+
renderChips();
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
};
|
|
1064
|
+
}
|
|
1065
|
+
/** Build the whole widget DOM and wire it up (returns the appended root). */
|
|
1066
|
+
function buildWidget() {
|
|
875
1067
|
const root = document.createElement("div");
|
|
876
1068
|
root.className = "dsh-tw-note";
|
|
877
1069
|
const card = document.createElement("div");
|
|
@@ -893,10 +1085,8 @@ function mountNoteWidget() {
|
|
|
893
1085
|
const titleInput = document.createElement("input");
|
|
894
1086
|
titleInput.className = "dsh-tw-note-title";
|
|
895
1087
|
titleInput.placeholder = "标题(默认时间戳)";
|
|
896
|
-
const
|
|
897
|
-
|
|
898
|
-
tagInput.placeholder = "tag(默认 inbox)";
|
|
899
|
-
fields.append(titleInput, tagInput);
|
|
1088
|
+
const tagEditor = buildTagEditor();
|
|
1089
|
+
fields.append(titleInput, tagEditor.el);
|
|
900
1090
|
const textarea = document.createElement("textarea");
|
|
901
1091
|
textarea.className = "dsh-tw-note-text";
|
|
902
1092
|
textarea.placeholder = "写点东西…(Ctrl+Enter 保存)";
|
|
@@ -931,10 +1121,9 @@ function mountNoteWidget() {
|
|
|
931
1121
|
card.hidden = false;
|
|
932
1122
|
opened = true;
|
|
933
1123
|
resetTitle();
|
|
934
|
-
if (tagInput.value.length === 0) tagInput.value = defaultTag;
|
|
935
1124
|
textarea.focus();
|
|
936
|
-
defaultTag = await
|
|
937
|
-
|
|
1125
|
+
defaultTag = (await fetchUiOptions()).defaultTag;
|
|
1126
|
+
tagEditor.setDefault(defaultTag);
|
|
938
1127
|
};
|
|
939
1128
|
const close = () => {
|
|
940
1129
|
card.hidden = true;
|
|
@@ -958,7 +1147,7 @@ function mountNoteWidget() {
|
|
|
958
1147
|
headers: { "content-type": "application/json" },
|
|
959
1148
|
body: JSON.stringify({
|
|
960
1149
|
title: titleInput.value.trim(),
|
|
961
|
-
|
|
1150
|
+
tags: tagEditor.getTags(),
|
|
962
1151
|
text
|
|
963
1152
|
}),
|
|
964
1153
|
signal: AbortSignal.timeout(1e4)
|
|
@@ -992,7 +1181,7 @@ function mountNoteWidget() {
|
|
|
992
1181
|
const doEdit = async () => {
|
|
993
1182
|
const title = titleInput.value.trim().length > 0 ? titleInput.value.trim() : timestampTitle();
|
|
994
1183
|
const text = textarea.value;
|
|
995
|
-
const
|
|
1184
|
+
const tags = tagEditor.getTags();
|
|
996
1185
|
edit.disabled = true;
|
|
997
1186
|
edit.textContent = "打开中…";
|
|
998
1187
|
try {
|
|
@@ -1001,7 +1190,7 @@ function mountNoteWidget() {
|
|
|
1001
1190
|
headers: { "content-type": "application/json" },
|
|
1002
1191
|
body: JSON.stringify({
|
|
1003
1192
|
title,
|
|
1004
|
-
|
|
1193
|
+
tags,
|
|
1005
1194
|
text
|
|
1006
1195
|
}),
|
|
1007
1196
|
signal: AbortSignal.timeout(1e4)
|
|
@@ -1027,10 +1216,27 @@ function mountNoteWidget() {
|
|
|
1027
1216
|
edit.addEventListener("click", () => {
|
|
1028
1217
|
doEdit();
|
|
1029
1218
|
});
|
|
1030
|
-
return
|
|
1031
|
-
|
|
1219
|
+
return root;
|
|
1220
|
+
}
|
|
1221
|
+
/**
|
|
1222
|
+
* Mount the floating quick-note widget. Fetches /status first: when
|
|
1223
|
+
* `ui.showQuickNote` is off the widget is never created (no DOM side effects).
|
|
1224
|
+
* Returns a disposer that removes it.
|
|
1225
|
+
*/
|
|
1226
|
+
function mountNoteWidget() {
|
|
1227
|
+
let disposed = false;
|
|
1228
|
+
let root;
|
|
1229
|
+
const disposer = () => {
|
|
1230
|
+
disposed = true;
|
|
1231
|
+
root?.remove();
|
|
1032
1232
|
document.querySelector(".dsh-tw-toast")?.remove();
|
|
1033
1233
|
};
|
|
1234
|
+
(async () => {
|
|
1235
|
+
const ui = await fetchUiOptions();
|
|
1236
|
+
if (disposed || !ui.showQuickNote) return;
|
|
1237
|
+
root = buildWidget();
|
|
1238
|
+
})();
|
|
1239
|
+
return disposer;
|
|
1034
1240
|
}
|
|
1035
1241
|
//#endregion
|
|
1036
1242
|
//#region src/client/settings-page.ts
|
|
@@ -1148,6 +1354,7 @@ function renderConfigSection(body, config, refresh) {
|
|
|
1148
1354
|
section.append(make("h3", "dsh-tw-settings-h", "常规配置"));
|
|
1149
1355
|
const note = config.note ?? {};
|
|
1150
1356
|
const git = config.git ?? {};
|
|
1357
|
+
const ui = config.ui ?? {};
|
|
1151
1358
|
const fields = [];
|
|
1152
1359
|
const textField = (key, label, initial) => {
|
|
1153
1360
|
const input = make("input", "dsh-tw-settings-input");
|
|
@@ -1198,6 +1405,8 @@ function renderConfigSection(body, config, refresh) {
|
|
|
1198
1405
|
numField("git.debounceMs", "自动 commit 防抖(ms)", typeof git.debounceMs === "number" ? git.debounceMs : 6e4);
|
|
1199
1406
|
textField("git.remote", "git 远端(空=仅本地)", typeof git.remote === "string" ? git.remote : "");
|
|
1200
1407
|
textField("git.branch", "git 分支", typeof git.branch === "string" ? git.branch : "main");
|
|
1408
|
+
checkField("ui.showQuickNote", "显示「快速笔记」悬浮按钮", ui.showQuickNote !== false);
|
|
1409
|
+
checkField("ui.showPanelStatus", "显示 TW 面板右下角「状态/重载」悬浮按钮", ui.showPanelStatus !== false);
|
|
1201
1410
|
const save = make("button", "dsh-tw-settings-btn dsh-tw-settings-primary", "保存配置");
|
|
1202
1411
|
save.type = "button";
|
|
1203
1412
|
save.addEventListener("click", () => {
|