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/lib/client.js
CHANGED
|
@@ -54,6 +54,21 @@ const CSS_TEXT = `
|
|
|
54
54
|
.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; }
|
|
55
55
|
.dsh-tw-entry svg { flex: none; }
|
|
56
56
|
.dsh-tw-entry .dsh-tw-entry-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
57
|
+
/* Collapsed rail: the shell narrows the sidebar to a 56px icon rail (dual
|
|
58
|
+
signals — the frame's data-sidebar-collapsed + the sidebar root's hashed
|
|
59
|
+
*_collapsed class, same doctrine as dsh-taskboard 0.4.3). Mirror the
|
|
60
|
+
native 36×36 icon-button geometry: center the icon, hide the label, and
|
|
61
|
+
scale it up to match the shell's 18px rail icons. */
|
|
62
|
+
[data-sidebar-collapsed] [data-dsh-tw-entry],
|
|
63
|
+
[class*="_collapsed"] [data-dsh-tw-entry] {
|
|
64
|
+
width: 36px; height: 36px; min-width: 36px;
|
|
65
|
+
margin: 0 0 12px; padding: 0;
|
|
66
|
+
justify-content: center; gap: 0; text-align: center;
|
|
67
|
+
}
|
|
68
|
+
[data-sidebar-collapsed] [data-dsh-tw-entry] .dsh-tw-entry-label,
|
|
69
|
+
[class*="_collapsed"] [data-dsh-tw-entry] .dsh-tw-entry-label { display: none; }
|
|
70
|
+
[data-sidebar-collapsed] [data-dsh-tw-entry] svg,
|
|
71
|
+
[class*="_collapsed"] [data-dsh-tw-entry] svg { width: 18px; height: 18px; }
|
|
57
72
|
|
|
58
73
|
/* ── center-column panel (fixed overlay, JS-pinned to the column rect) ── */
|
|
59
74
|
.dsh-tw-view {
|
|
@@ -66,31 +81,31 @@ html[data-dsh-tw-active] [data-pane="conversation"] > :not([data-dsh-tw-view]),
|
|
|
66
81
|
html[data-dsh-tw-active] [class*="centerCol"] > :not([data-dsh-tw-view]),
|
|
67
82
|
html[data-dsh-tw-active] .dshDesktopConversationSurface > :not([data-dsh-tw-view]) { display: none !important; }
|
|
68
83
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
84
|
+
/* ── bottom-right status / reload floating buttons ───────────────────
|
|
85
|
+
The top panel bar (title + status + reload row) is gone so TW's menubar
|
|
86
|
+
reaches the panel top; status/reload now live here as floating buttons,
|
|
87
|
+
same visual language as the quick-note toggle. The ui.showPanelStatus
|
|
88
|
+
setting controls whether they mount at all. */
|
|
89
|
+
.dsh-tw-panel-status {
|
|
90
|
+
/* 状态/重载悬浮按钮:右对齐到与快速笔记按钮同一条竖线(right:24),
|
|
91
|
+
垂直排在快速笔记按钮(bottom:88,高约 38px)上方;
|
|
92
|
+
两个按钮竖直依次排列(状态在上、重载在下),与其他插件按钮列对齐、不堆叠。 */
|
|
93
|
+
position: fixed; right: 24px; bottom: 140px; z-index: 950;
|
|
94
|
+
display: flex; flex-direction: column; align-items: flex-end; gap: 8px;
|
|
75
95
|
}
|
|
76
|
-
.dsh-tw-panel-
|
|
77
|
-
border: 1px solid var(--dsw-alias-border-l2, rgba(
|
|
78
|
-
background:
|
|
79
|
-
padding:
|
|
96
|
+
.dsh-tw-panel-status-btn {
|
|
97
|
+
border: 1px solid var(--dsw-alias-border-l2, rgba(0,0,0,.18));
|
|
98
|
+
background: var(--dsw-alias-bg-layer-2, #fff); color: var(--dsw-alias-label-primary, #222);
|
|
99
|
+
font: inherit; font-size: 12px; padding: 7px 12px; border-radius: 999px;
|
|
100
|
+
box-shadow: var(--dsw-shadow-lv3, 0 4px 16px rgba(0,0,0,.16)); cursor: pointer;
|
|
101
|
+
display: inline-flex; align-items: center; gap: 6px;
|
|
102
|
+
transition: background-color 120ms ease;
|
|
80
103
|
}
|
|
81
|
-
.dsh-tw-panel-
|
|
82
|
-
.dsh-tw-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
cluster's width at the panel bar's right end — mirroring how
|
|
87
|
-
dsh-better-sidebar pads the DSH session header — and the panel's own
|
|
88
|
-
状态/重载 buttons never sit underneath it. */
|
|
89
|
-
.dsh-tw-view[data-sidebar-host] .dsh-tw-panel-bar { padding-right: 80px; }
|
|
90
|
-
.dsh-tw-status-chip { font-variant-numeric: tabular-nums; }
|
|
91
|
-
.dsh-tw-status-chip[data-state="running"] { color: var(--dsw-alias-state-success-primary, #3eaa5f); }
|
|
92
|
-
.dsh-tw-status-chip[data-state="starting"] { color: var(--dsw-alias-state-warning-primary, #d9822b); }
|
|
93
|
-
.dsh-tw-status-chip[data-state="failed"], .dsh-tw-status-chip[data-state="stopped"] { color: var(--dsw-alias-state-error-primary, #d13b3b); }
|
|
104
|
+
.dsh-tw-panel-status-btn:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,.1)); }
|
|
105
|
+
.dsh-tw-status-dot { width: 8px; height: 8px; border-radius: 50%; background: #999; flex: none; }
|
|
106
|
+
.dsh-tw-status-dot[data-state="running"] { background: var(--dsw-alias-state-success-primary, #3eaa5f); }
|
|
107
|
+
.dsh-tw-status-dot[data-state="starting"] { background: var(--dsw-alias-state-warning-primary, #d9822b); }
|
|
108
|
+
.dsh-tw-status-dot[data-state="failed"], .dsh-tw-status-dot[data-state="stopped"] { background: var(--dsw-alias-state-error-primary, #d13b3b); }
|
|
94
109
|
|
|
95
110
|
.dsh-tw-panel-frame { flex: 1; min-height: 0; border: 0; width: 100%; display: block; background: #fff; }
|
|
96
111
|
/* [hidden] must beat the author display rules above (UA hidden is overridden). */
|
|
@@ -169,6 +184,34 @@ html[data-dsh-tw-active] .dshDesktopConversationSurface > :not([data-dsh-tw-view
|
|
|
169
184
|
}
|
|
170
185
|
.dsh-tw-note-edit:hover:not(:disabled) { filter: brightness(.96); }
|
|
171
186
|
.dsh-tw-note-edit:disabled { opacity: .55; cursor: default; }
|
|
187
|
+
/* ── Multi-tag chip editor (quick-note tags) ─────────────────────────────── */
|
|
188
|
+
.dsh-tw-note-tags { position: relative; flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 4px; }
|
|
189
|
+
.dsh-tw-note-chips { display: flex; flex-wrap: wrap; gap: 4px; }
|
|
190
|
+
.dsh-tw-note-tagchip {
|
|
191
|
+
display: inline-flex; align-items: center; gap: 4px;
|
|
192
|
+
padding: 2px 6px 2px 8px; border-radius: 999px; font-size: 11px; line-height: 1.4;
|
|
193
|
+
background: color-mix(in srgb, var(--dsw-alias-brand-primary, #3e63dd) 10%, var(--dsw-alias-bg-layer-1, #fff));
|
|
194
|
+
border: 1px solid color-mix(in srgb, var(--dsw-alias-brand-primary, #3e63dd) 30%, transparent);
|
|
195
|
+
color: var(--dsw-alias-label-primary, #222);
|
|
196
|
+
}
|
|
197
|
+
.dsh-tw-note-tagchip-x {
|
|
198
|
+
cursor: pointer; font-size: 12px; line-height: 1; padding: 0 2px; border-radius: 50%;
|
|
199
|
+
color: var(--dsw-alias-label-secondary, #888);
|
|
200
|
+
}
|
|
201
|
+
.dsh-tw-note-tagchip-x:hover { color: var(--dsw-alias-state-error-primary, #d13b3b); }
|
|
202
|
+
.dsh-tw-note-taginput { width: 100%; box-sizing: border-box; }
|
|
203
|
+
.dsh-tw-note-tagsuggest {
|
|
204
|
+
position: absolute; left: 0; right: 0; top: calc(100% + 2px); z-index: 60;
|
|
205
|
+
max-height: 168px; overflow-y: auto; border-radius: 8px; padding: 4px;
|
|
206
|
+
background: var(--dsw-alias-bg-layer-2, #fff); color: var(--dsw-alias-label-primary, #222);
|
|
207
|
+
border: 1px solid var(--dsw-alias-border-l2, rgba(0,0,0,.18));
|
|
208
|
+
box-shadow: var(--dsw-shadow-lv2, 0 4px 16px rgba(0,0,0,.16));
|
|
209
|
+
font-size: 12px;
|
|
210
|
+
}
|
|
211
|
+
.dsh-tw-note-tagsuggest-item {
|
|
212
|
+
padding: 6px 9px; border-radius: 6px; cursor: pointer; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
|
213
|
+
}
|
|
214
|
+
.dsh-tw-note-tagsuggest-item:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,.12)); }
|
|
172
215
|
/* ── Native-editor popup iframe (quick-note "在 TW 中编辑") ──────────────── */
|
|
173
216
|
.dsh-tw-editor-popup {
|
|
174
217
|
position: fixed; left: 0; right: 0; top: 0; bottom: 0; margin: auto;
|
|
@@ -503,34 +546,14 @@ function mountPanel(state) {
|
|
|
503
546
|
let frameArea;
|
|
504
547
|
let errorArea;
|
|
505
548
|
let chip;
|
|
549
|
+
let statusDot;
|
|
550
|
+
let statusFloater;
|
|
506
551
|
let refreshTimer;
|
|
507
552
|
let refreshAttempts = 0;
|
|
508
553
|
const build = () => {
|
|
509
554
|
const view = document.createElement("div");
|
|
510
555
|
view.dataset.dshTwView = "";
|
|
511
556
|
view.className = "dsh-tw-view";
|
|
512
|
-
const bar = document.createElement("div");
|
|
513
|
-
bar.className = "dsh-tw-panel-bar";
|
|
514
|
-
const title = document.createElement("span");
|
|
515
|
-
title.className = "dsh-tw-panel-title";
|
|
516
|
-
title.textContent = "TiddlyWiki 知识库";
|
|
517
|
-
chip = document.createElement("span");
|
|
518
|
-
chip.className = "dsh-tw-status-chip";
|
|
519
|
-
chip.dataset.state = "unknown";
|
|
520
|
-
chip.textContent = "—";
|
|
521
|
-
const reload = document.createElement("button");
|
|
522
|
-
reload.type = "button";
|
|
523
|
-
reload.textContent = "重载";
|
|
524
|
-
reload.addEventListener("click", () => {
|
|
525
|
-
if (iframe !== void 0 && !iframe.hidden) iframe.src = iframe.src;
|
|
526
|
-
});
|
|
527
|
-
const refresh = document.createElement("button");
|
|
528
|
-
refresh.type = "button";
|
|
529
|
-
refresh.textContent = "状态";
|
|
530
|
-
refresh.addEventListener("click", () => {
|
|
531
|
-
doRefresh();
|
|
532
|
-
});
|
|
533
|
-
bar.append(title, chip, refresh, reload);
|
|
534
557
|
frameArea = document.createElement("div");
|
|
535
558
|
frameArea.className = "dsh-tw-panel-frame-wrap";
|
|
536
559
|
frameArea.style.cssText = "flex:1;min-height:0;display:flex;flex-direction:column";
|
|
@@ -542,13 +565,47 @@ function mountPanel(state) {
|
|
|
542
565
|
errorArea = document.createElement("div");
|
|
543
566
|
errorArea.className = "dsh-tw-panel-error";
|
|
544
567
|
errorArea.hidden = true;
|
|
545
|
-
view.append(
|
|
568
|
+
view.append(frameArea, errorArea);
|
|
546
569
|
return view;
|
|
547
570
|
};
|
|
571
|
+
/**
|
|
572
|
+
* Bottom-right floating status/reload buttons (same visual language as the
|
|
573
|
+
* quick-note toggle) — the top panel bar is gone, so TW's menubar reaches
|
|
574
|
+
* the very top of the panel and clears the host's top-right buttons. Mount
|
|
575
|
+
* is gated by `ui.showPanelStatus` (settings page toggle).
|
|
576
|
+
*/
|
|
577
|
+
const buildStatusFloater = () => {
|
|
578
|
+
const wrap = document.createElement("div");
|
|
579
|
+
wrap.className = "dsh-tw-panel-status";
|
|
580
|
+
const statusBtn = document.createElement("button");
|
|
581
|
+
statusBtn.type = "button";
|
|
582
|
+
statusBtn.className = "dsh-tw-panel-status-btn";
|
|
583
|
+
statusBtn.title = "刷新状态";
|
|
584
|
+
statusDot = document.createElement("span");
|
|
585
|
+
statusDot.className = "dsh-tw-status-dot";
|
|
586
|
+
statusDot.dataset.state = "unknown";
|
|
587
|
+
chip = document.createElement("span");
|
|
588
|
+
chip.className = "dsh-tw-panel-status-text";
|
|
589
|
+
chip.textContent = "—";
|
|
590
|
+
statusBtn.append(statusDot, chip);
|
|
591
|
+
statusBtn.addEventListener("click", () => {
|
|
592
|
+
doRefresh();
|
|
593
|
+
});
|
|
594
|
+
const reloadBtn = document.createElement("button");
|
|
595
|
+
reloadBtn.type = "button";
|
|
596
|
+
reloadBtn.className = "dsh-tw-panel-status-btn";
|
|
597
|
+
reloadBtn.textContent = "重载";
|
|
598
|
+
reloadBtn.title = "重载 TiddlyWiki 面板";
|
|
599
|
+
reloadBtn.addEventListener("click", () => {
|
|
600
|
+
if (iframe !== void 0 && !iframe.hidden) iframe.src = iframe.src;
|
|
601
|
+
});
|
|
602
|
+
wrap.append(statusBtn, reloadBtn);
|
|
603
|
+
document.body.append(wrap);
|
|
604
|
+
return wrap;
|
|
605
|
+
};
|
|
548
606
|
const setChip = (stateName, text) => {
|
|
549
|
-
if (
|
|
550
|
-
chip.
|
|
551
|
-
chip.textContent = text;
|
|
607
|
+
if (statusDot !== void 0) statusDot.dataset.state = stateName;
|
|
608
|
+
if (chip !== void 0) chip.textContent = text;
|
|
552
609
|
};
|
|
553
610
|
/** Pin the overlay to the center column's current viewport rect. */
|
|
554
611
|
const syncRect = () => {
|
|
@@ -702,6 +759,10 @@ function mountPanel(state) {
|
|
|
702
759
|
const unsubscribe = state.subscribe(applyActive);
|
|
703
760
|
ensure();
|
|
704
761
|
applyActive();
|
|
762
|
+
(async () => {
|
|
763
|
+
if ((await fetchStatus())?.ui?.showPanelStatus === false) return;
|
|
764
|
+
statusFloater = buildStatusFloater();
|
|
765
|
+
})();
|
|
705
766
|
return () => {
|
|
706
767
|
if (refreshTimer !== void 0) window.clearTimeout(refreshTimer);
|
|
707
768
|
window.clearInterval(syncInterval);
|
|
@@ -714,6 +775,7 @@ function mountPanel(state) {
|
|
|
714
775
|
unsubscribe();
|
|
715
776
|
document.documentElement.removeAttribute(ACTIVE_ATTR);
|
|
716
777
|
container?.remove();
|
|
778
|
+
statusFloater?.remove();
|
|
717
779
|
};
|
|
718
780
|
}
|
|
719
781
|
//#endregion
|
|
@@ -853,14 +915,23 @@ function ensurePopup() {
|
|
|
853
915
|
* collapsible, independent of any shell DOM. Lets the human jot drafts /
|
|
854
916
|
* scratch notes while waiting for the AI or drafting the next prompt.
|
|
855
917
|
*
|
|
856
|
-
* Save posts to /dsh-tiddlywiki/note → an independent tiddler (title &
|
|
918
|
+
* Save posts to /dsh-tiddlywiki/note → an independent tiddler (title & tags
|
|
857
919
|
* editable; defaults: timestamp title + config tag, usually "inbox").
|
|
858
920
|
*
|
|
921
|
+
* ui.showQuickNote config: the widget is mounted async and stays hidden (and
|
|
922
|
+
* never appended) when the option is off, so the toggle button can be disabled
|
|
923
|
+
* from the settings page without touching the shell.
|
|
924
|
+
*
|
|
925
|
+
* Tag editor: multi-select chips + autocomplete from the wiki's existing tags
|
|
926
|
+
* (GET /dsh-tiddlywiki/tags). Enter/`,` commits the typed value; Backspace on
|
|
927
|
+
* an empty field removes the last chip; the dropdown filters on typing.
|
|
928
|
+
*
|
|
859
929
|
* @module dsh-tiddlywiki/client/note-widget
|
|
860
930
|
*/
|
|
861
931
|
const NOTE_ENDPOINT = "/dsh-tiddlywiki/note";
|
|
862
932
|
const EDIT_ENDPOINT = "/dsh-tiddlywiki/edit";
|
|
863
933
|
const STATUS_ENDPOINT = "/dsh-tiddlywiki/status";
|
|
934
|
+
const TAGS_ENDPOINT = "/dsh-tiddlywiki/tags";
|
|
864
935
|
function pad(n) {
|
|
865
936
|
return n < 10 ? `0${n}` : String(n);
|
|
866
937
|
}
|
|
@@ -868,16 +939,137 @@ function pad(n) {
|
|
|
868
939
|
function timestampTitle(date = /* @__PURE__ */ new Date()) {
|
|
869
940
|
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}`;
|
|
870
941
|
}
|
|
871
|
-
async function
|
|
942
|
+
async function fetchUiOptions() {
|
|
943
|
+
const fallback = {
|
|
944
|
+
showQuickNote: true,
|
|
945
|
+
defaultTag: "inbox"
|
|
946
|
+
};
|
|
872
947
|
try {
|
|
873
948
|
const res = await fetch(STATUS_ENDPOINT, { signal: AbortSignal.timeout(5e3) });
|
|
874
|
-
if (!res.ok) return
|
|
875
|
-
|
|
949
|
+
if (!res.ok) return fallback;
|
|
950
|
+
const payload = await res.json();
|
|
951
|
+
return {
|
|
952
|
+
showQuickNote: payload.ui?.showQuickNote !== false,
|
|
953
|
+
defaultTag: payload.note?.tag ?? "inbox"
|
|
954
|
+
};
|
|
876
955
|
} catch {
|
|
877
|
-
return
|
|
956
|
+
return fallback;
|
|
878
957
|
}
|
|
879
958
|
}
|
|
880
|
-
|
|
959
|
+
/** Multi-tag chip editor with autocomplete from the wiki's existing tags. */
|
|
960
|
+
function buildTagEditor() {
|
|
961
|
+
const wrap = document.createElement("div");
|
|
962
|
+
wrap.className = "dsh-tw-note-tags";
|
|
963
|
+
const chipWrap = document.createElement("div");
|
|
964
|
+
chipWrap.className = "dsh-tw-note-chips";
|
|
965
|
+
const input = document.createElement("input");
|
|
966
|
+
input.className = "dsh-tw-note-taginput";
|
|
967
|
+
input.placeholder = "tag(可多选,自动补全)";
|
|
968
|
+
const suggest = document.createElement("div");
|
|
969
|
+
suggest.className = "dsh-tw-note-tagsuggest";
|
|
970
|
+
suggest.hidden = true;
|
|
971
|
+
wrap.append(chipWrap, input, suggest);
|
|
972
|
+
const chips = [];
|
|
973
|
+
const hideSuggest = () => {
|
|
974
|
+
suggest.hidden = true;
|
|
975
|
+
};
|
|
976
|
+
const renderChips = () => {
|
|
977
|
+
chipWrap.replaceChildren();
|
|
978
|
+
for (const tag of chips) {
|
|
979
|
+
const chip = document.createElement("span");
|
|
980
|
+
chip.className = "dsh-tw-note-tagchip";
|
|
981
|
+
chip.textContent = tag;
|
|
982
|
+
const x = document.createElement("span");
|
|
983
|
+
x.className = "dsh-tw-note-tagchip-x";
|
|
984
|
+
x.textContent = "×";
|
|
985
|
+
x.title = `移除 tag「${tag}」`;
|
|
986
|
+
x.addEventListener("click", (event) => {
|
|
987
|
+
event.stopPropagation();
|
|
988
|
+
const i = chips.indexOf(tag);
|
|
989
|
+
if (i >= 0) {
|
|
990
|
+
chips.splice(i, 1);
|
|
991
|
+
renderChips();
|
|
992
|
+
}
|
|
993
|
+
});
|
|
994
|
+
chip.append(x);
|
|
995
|
+
chipWrap.append(chip);
|
|
996
|
+
}
|
|
997
|
+
};
|
|
998
|
+
const addTag = (tag) => {
|
|
999
|
+
const t = tag.trim();
|
|
1000
|
+
if (t.length === 0 || chips.includes(t)) return;
|
|
1001
|
+
chips.push(t);
|
|
1002
|
+
input.value = "";
|
|
1003
|
+
renderChips();
|
|
1004
|
+
hideSuggest();
|
|
1005
|
+
input.focus();
|
|
1006
|
+
};
|
|
1007
|
+
const commitInput = () => {
|
|
1008
|
+
for (const raw of input.value.split(/\s+/)) addTag(raw);
|
|
1009
|
+
};
|
|
1010
|
+
let knownTags = [];
|
|
1011
|
+
let tagsPromise;
|
|
1012
|
+
const ensureTags = () => {
|
|
1013
|
+
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(() => []);
|
|
1014
|
+
tagsPromise.then((list) => {
|
|
1015
|
+
knownTags = list;
|
|
1016
|
+
});
|
|
1017
|
+
return tagsPromise;
|
|
1018
|
+
};
|
|
1019
|
+
const showSuggest = () => {
|
|
1020
|
+
const q = input.value.trim().toLowerCase();
|
|
1021
|
+
const matches = knownTags.filter((t) => !chips.includes(t) && (q.length === 0 || t.toLowerCase().includes(q))).slice(0, 8);
|
|
1022
|
+
suggest.replaceChildren();
|
|
1023
|
+
for (const tag of matches) {
|
|
1024
|
+
const item = document.createElement("div");
|
|
1025
|
+
item.className = "dsh-tw-note-tagsuggest-item";
|
|
1026
|
+
item.textContent = tag;
|
|
1027
|
+
item.addEventListener("mousedown", (event) => {
|
|
1028
|
+
event.preventDefault();
|
|
1029
|
+
addTag(tag);
|
|
1030
|
+
});
|
|
1031
|
+
suggest.append(item);
|
|
1032
|
+
}
|
|
1033
|
+
suggest.hidden = matches.length === 0;
|
|
1034
|
+
};
|
|
1035
|
+
input.addEventListener("focus", () => {
|
|
1036
|
+
ensureTags().then(showSuggest);
|
|
1037
|
+
});
|
|
1038
|
+
input.addEventListener("input", () => {
|
|
1039
|
+
if (knownTags.length === 0) ensureTags().then(showSuggest);
|
|
1040
|
+
else showSuggest();
|
|
1041
|
+
});
|
|
1042
|
+
input.addEventListener("keydown", (event) => {
|
|
1043
|
+
if (event.key === "Enter" || event.key === ",") {
|
|
1044
|
+
event.preventDefault();
|
|
1045
|
+
commitInput();
|
|
1046
|
+
} else if (event.key === "Backspace" && input.value.length === 0 && chips.length > 0) {
|
|
1047
|
+
chips.pop();
|
|
1048
|
+
renderChips();
|
|
1049
|
+
} else if (event.key === "Escape") hideSuggest();
|
|
1050
|
+
});
|
|
1051
|
+
document.addEventListener("click", (event) => {
|
|
1052
|
+
if (!wrap.contains(event.target)) hideSuggest();
|
|
1053
|
+
}, true);
|
|
1054
|
+
return {
|
|
1055
|
+
el: wrap,
|
|
1056
|
+
getTags: () => {
|
|
1057
|
+
commitInput();
|
|
1058
|
+
return [...chips];
|
|
1059
|
+
},
|
|
1060
|
+
setDefault: (tag) => {
|
|
1061
|
+
if (chips.length === 0) {
|
|
1062
|
+
const t = tag.trim();
|
|
1063
|
+
if (t.length > 0) {
|
|
1064
|
+
chips.push(t);
|
|
1065
|
+
renderChips();
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
};
|
|
1070
|
+
}
|
|
1071
|
+
/** Build the whole widget DOM and wire it up (returns the appended root). */
|
|
1072
|
+
function buildWidget() {
|
|
881
1073
|
const root = document.createElement("div");
|
|
882
1074
|
root.className = "dsh-tw-note";
|
|
883
1075
|
const card = document.createElement("div");
|
|
@@ -899,10 +1091,8 @@ function mountNoteWidget() {
|
|
|
899
1091
|
const titleInput = document.createElement("input");
|
|
900
1092
|
titleInput.className = "dsh-tw-note-title";
|
|
901
1093
|
titleInput.placeholder = "标题(默认时间戳)";
|
|
902
|
-
const
|
|
903
|
-
|
|
904
|
-
tagInput.placeholder = "tag(默认 inbox)";
|
|
905
|
-
fields.append(titleInput, tagInput);
|
|
1094
|
+
const tagEditor = buildTagEditor();
|
|
1095
|
+
fields.append(titleInput, tagEditor.el);
|
|
906
1096
|
const textarea = document.createElement("textarea");
|
|
907
1097
|
textarea.className = "dsh-tw-note-text";
|
|
908
1098
|
textarea.placeholder = "写点东西…(Ctrl+Enter 保存)";
|
|
@@ -937,10 +1127,9 @@ function mountNoteWidget() {
|
|
|
937
1127
|
card.hidden = false;
|
|
938
1128
|
opened = true;
|
|
939
1129
|
resetTitle();
|
|
940
|
-
if (tagInput.value.length === 0) tagInput.value = defaultTag;
|
|
941
1130
|
textarea.focus();
|
|
942
|
-
defaultTag = await
|
|
943
|
-
|
|
1131
|
+
defaultTag = (await fetchUiOptions()).defaultTag;
|
|
1132
|
+
tagEditor.setDefault(defaultTag);
|
|
944
1133
|
};
|
|
945
1134
|
const close = () => {
|
|
946
1135
|
card.hidden = true;
|
|
@@ -964,7 +1153,7 @@ function mountNoteWidget() {
|
|
|
964
1153
|
headers: { "content-type": "application/json" },
|
|
965
1154
|
body: JSON.stringify({
|
|
966
1155
|
title: titleInput.value.trim(),
|
|
967
|
-
|
|
1156
|
+
tags: tagEditor.getTags(),
|
|
968
1157
|
text
|
|
969
1158
|
}),
|
|
970
1159
|
signal: AbortSignal.timeout(1e4)
|
|
@@ -998,7 +1187,7 @@ function mountNoteWidget() {
|
|
|
998
1187
|
const doEdit = async () => {
|
|
999
1188
|
const title = titleInput.value.trim().length > 0 ? titleInput.value.trim() : timestampTitle();
|
|
1000
1189
|
const text = textarea.value;
|
|
1001
|
-
const
|
|
1190
|
+
const tags = tagEditor.getTags();
|
|
1002
1191
|
edit.disabled = true;
|
|
1003
1192
|
edit.textContent = "打开中…";
|
|
1004
1193
|
try {
|
|
@@ -1007,7 +1196,7 @@ function mountNoteWidget() {
|
|
|
1007
1196
|
headers: { "content-type": "application/json" },
|
|
1008
1197
|
body: JSON.stringify({
|
|
1009
1198
|
title,
|
|
1010
|
-
|
|
1199
|
+
tags,
|
|
1011
1200
|
text
|
|
1012
1201
|
}),
|
|
1013
1202
|
signal: AbortSignal.timeout(1e4)
|
|
@@ -1033,10 +1222,27 @@ function mountNoteWidget() {
|
|
|
1033
1222
|
edit.addEventListener("click", () => {
|
|
1034
1223
|
doEdit();
|
|
1035
1224
|
});
|
|
1036
|
-
return
|
|
1037
|
-
|
|
1225
|
+
return root;
|
|
1226
|
+
}
|
|
1227
|
+
/**
|
|
1228
|
+
* Mount the floating quick-note widget. Fetches /status first: when
|
|
1229
|
+
* `ui.showQuickNote` is off the widget is never created (no DOM side effects).
|
|
1230
|
+
* Returns a disposer that removes it.
|
|
1231
|
+
*/
|
|
1232
|
+
function mountNoteWidget() {
|
|
1233
|
+
let disposed = false;
|
|
1234
|
+
let root;
|
|
1235
|
+
const disposer = () => {
|
|
1236
|
+
disposed = true;
|
|
1237
|
+
root?.remove();
|
|
1038
1238
|
document.querySelector(".dsh-tw-toast")?.remove();
|
|
1039
1239
|
};
|
|
1240
|
+
(async () => {
|
|
1241
|
+
const ui = await fetchUiOptions();
|
|
1242
|
+
if (disposed || !ui.showQuickNote) return;
|
|
1243
|
+
root = buildWidget();
|
|
1244
|
+
})();
|
|
1245
|
+
return disposer;
|
|
1040
1246
|
}
|
|
1041
1247
|
//#endregion
|
|
1042
1248
|
//#region src/client/settings-page.ts
|
|
@@ -1154,6 +1360,7 @@ function renderConfigSection(body, config, refresh) {
|
|
|
1154
1360
|
section.append(make("h3", "dsh-tw-settings-h", "常规配置"));
|
|
1155
1361
|
const note = config.note ?? {};
|
|
1156
1362
|
const git = config.git ?? {};
|
|
1363
|
+
const ui = config.ui ?? {};
|
|
1157
1364
|
const fields = [];
|
|
1158
1365
|
const textField = (key, label, initial) => {
|
|
1159
1366
|
const input = make("input", "dsh-tw-settings-input");
|
|
@@ -1204,6 +1411,8 @@ function renderConfigSection(body, config, refresh) {
|
|
|
1204
1411
|
numField("git.debounceMs", "自动 commit 防抖(ms)", typeof git.debounceMs === "number" ? git.debounceMs : 6e4);
|
|
1205
1412
|
textField("git.remote", "git 远端(空=仅本地)", typeof git.remote === "string" ? git.remote : "");
|
|
1206
1413
|
textField("git.branch", "git 分支", typeof git.branch === "string" ? git.branch : "main");
|
|
1414
|
+
checkField("ui.showQuickNote", "显示「快速笔记」悬浮按钮", ui.showQuickNote !== false);
|
|
1415
|
+
checkField("ui.showPanelStatus", "显示 TW 面板右下角「状态/重载」悬浮按钮", ui.showPanelStatus !== false);
|
|
1207
1416
|
const save = make("button", "dsh-tw-settings-btn dsh-tw-settings-primary", "保存配置");
|
|
1208
1417
|
save.type = "button";
|
|
1209
1418
|
save.addEventListener("click", () => {
|