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/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
- .dsh-tw-panel-bar {
70
- display: flex; align-items: center; gap: 8px; padding: 6px 10px;
71
- border-bottom: 1px solid var(--dsw-alias-border-l2, rgba(128,128,128,.18));
72
- background: var(--dsw-alias-bg-layer-1, transparent);
73
- font-size: 12px; color: var(--dsw-alias-label-secondary, inherit);
74
- flex: none;
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-bar button {
77
- border: 1px solid var(--dsw-alias-border-l2, rgba(128,128,128,.25));
78
- background: transparent; color: inherit; font: inherit; font-size: 12px;
79
- padding: 2px 8px; border-radius: 6px; cursor: pointer;
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-bar button:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,.12)); }
82
- .dsh-tw-panel-title { font-weight: 600; margin-right: auto; }
83
- /* dsh-better-sidebar coexistence: the floating toggle cluster (two 28px
84
- buttons at the viewport's top-right) lives above the panel (z-index is
85
- handled in JS — the panel stays below the host layer), so reserve the
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(bar, frameArea, errorArea);
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 (chip === void 0) return;
550
- chip.dataset.state = stateName;
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 & tag
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 fetchDefaultTag() {
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 "inbox";
875
- return (await res.json()).note?.tag ?? "inbox";
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 "inbox";
956
+ return fallback;
878
957
  }
879
958
  }
880
- function mountNoteWidget() {
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 tagInput = document.createElement("input");
903
- tagInput.className = "dsh-tw-note-tag";
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 fetchDefaultTag();
943
- if (tagInput.value.length === 0) tagInput.value = defaultTag;
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
- tag: tagInput.value.trim(),
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 tag = tagInput.value.trim();
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
- tag,
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
- root.remove();
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", () => {