dsh-tiddlywiki 0.1.1 → 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.
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 {
@@ -80,6 +95,13 @@ html[data-dsh-tw-active] .dshDesktopConversationSurface > :not([data-dsh-tw-view
80
95
  }
81
96
  .dsh-tw-panel-bar button:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,.12)); }
82
97
  .dsh-tw-panel-title { font-weight: 600; margin-right: auto; }
98
+ /* dsh-better-sidebar coexistence: the floating toggle cluster (two 28px
99
+ buttons at the viewport's top-right) lives above the panel (z-index is
100
+ handled in JS — the panel stays below the host layer), so reserve the
101
+ cluster's width at the panel bar's right end — mirroring how
102
+ dsh-better-sidebar pads the DSH session header — and the panel's own
103
+ 状态/重载 buttons never sit underneath it. */
104
+ .dsh-tw-view[data-sidebar-host] .dsh-tw-panel-bar { padding-right: 80px; }
83
105
  .dsh-tw-status-chip { font-variant-numeric: tabular-nums; }
84
106
  .dsh-tw-status-chip[data-state="running"] { color: var(--dsw-alias-state-success-primary, #3eaa5f); }
85
107
  .dsh-tw-status-chip[data-state="starting"] { color: var(--dsw-alias-state-warning-primary, #d9822b); }
@@ -162,6 +184,34 @@ html[data-dsh-tw-active] .dshDesktopConversationSurface > :not([data-dsh-tw-view
162
184
  }
163
185
  .dsh-tw-note-edit:hover:not(:disabled) { filter: brightness(.96); }
164
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)); }
165
215
  /* ── Native-editor popup iframe (quick-note "在 TW 中编辑") ──────────────── */
166
216
  .dsh-tw-editor-popup {
167
217
  position: fixed; left: 0; right: 0; top: 0; bottom: 0; margin: auto;
@@ -435,10 +485,35 @@ const ACTIVATE_EVENT = "dsh-panel-activate";
435
485
  const PANEL_NAME = "dsh-tiddlywiki";
436
486
  /** Overlay z-index: above the shell content, below the note widget (950). */
437
487
  const PANEL_Z_INDEX = 40;
488
+ /**
489
+ * dsh-better-sidebar's unified fixed host layer (its persistent
490
+ * expand/collapse toggle cluster lives inside it, at a global z-index of 25
491
+ * — its internal 45 is trapped by the host's stacking context). When this
492
+ * host is present the panel must stay BELOW it, so the sidebar toggle
493
+ * buttons stay visible and clickable above the full-screen TW panel.
494
+ */
495
+ const PANEL_HOST_SELECTOR = "[data-dsh-panel-host]";
496
+ /** The app's own shell overlay layer (dsh-client-ui-layout pins it at 20). */
497
+ const APP_OVERLAY_Z_INDEX = 20;
438
498
  /** Safety re-measure cadence for shell layout changes CSS can't see. */
439
499
  const SYNC_INTERVAL_MS = 2e3;
440
500
  const STATUS_ENDPOINT$1 = "/dsh-tiddlywiki/status";
441
501
  const RESTART_ENDPOINT$1 = "/dsh-tiddlywiki/restart";
502
+ /**
503
+ * The panel's z-index: below any dsh-better-sidebar host layer so its
504
+ * persistent toggle cluster (top-right corner) stays visible and clickable
505
+ * above the full-screen TW panel, otherwise the default 40. The host's live
506
+ * computed z-index is read instead of hardcoding 25, so the rule tracks
507
+ * plugin updates; the panel is still clamped above the app's own shell
508
+ * overlay layer (dsh-client-ui-layout pins it at 20).
509
+ */
510
+ function resolvePanelZIndex() {
511
+ const host = document.querySelector(PANEL_HOST_SELECTOR);
512
+ if (host === null) return PANEL_Z_INDEX;
513
+ const parsed = parseInt(getComputedStyle(host).zIndex, 10);
514
+ if (!Number.isFinite(parsed)) return PANEL_Z_INDEX;
515
+ return Math.max(APP_OVERLAY_Z_INDEX, Math.min(PANEL_Z_INDEX, parsed - 1));
516
+ }
442
517
  function conversationColumn() {
443
518
  for (const selector of COLUMN_SELECTORS) {
444
519
  const el = document.querySelector(selector);
@@ -532,13 +607,25 @@ function mountPanel(state) {
532
607
  if (container.style.width !== width) container.style.width = width;
533
608
  if (container.style.height !== height) container.style.height = height;
534
609
  };
610
+ /**
611
+ * Coexist with dsh-better-sidebar: keep the panel's z-index below the
612
+ * host layer (so its toggle cluster stays clickable above the panel) and
613
+ * flag the host's presence so CSS can reserve the cluster's width at the
614
+ * panel bar's right end. Re-run whenever the host mounts/unmounts.
615
+ */
616
+ const applyChrome = () => {
617
+ if (container === void 0) return;
618
+ container.style.zIndex = String(resolvePanelZIndex());
619
+ if (document.querySelector(PANEL_HOST_SELECTOR) !== null) container.dataset.sidebarHost = "1";
620
+ else delete container.dataset.sidebarHost;
621
+ };
535
622
  const ensure = () => {
536
623
  if (container !== void 0) return;
537
624
  columnEl = conversationColumn();
538
625
  if (columnEl === void 0) return;
539
626
  container = build();
540
627
  container.style.position = "fixed";
541
- container.style.zIndex = String(PANEL_Z_INDEX);
628
+ applyChrome();
542
629
  document.body.append(container);
543
630
  syncRect();
544
631
  };
@@ -640,6 +727,7 @@ function mountPanel(state) {
640
727
  };
641
728
  const waitObserver = new MutationObserver(() => {
642
729
  ensure();
730
+ applyChrome();
643
731
  });
644
732
  waitObserver.observe(document.body, {
645
733
  childList: true,
@@ -808,14 +896,23 @@ function ensurePopup() {
808
896
  * collapsible, independent of any shell DOM. Lets the human jot drafts /
809
897
  * scratch notes while waiting for the AI or drafting the next prompt.
810
898
  *
811
- * Save posts to /dsh-tiddlywiki/note → an independent tiddler (title & tag
899
+ * Save posts to /dsh-tiddlywiki/note → an independent tiddler (title & tags
812
900
  * editable; defaults: timestamp title + config tag, usually "inbox").
813
901
  *
902
+ * ui.showQuickNote config: the widget is mounted async and stays hidden (and
903
+ * never appended) when the option is off, so the toggle button can be disabled
904
+ * from the settings page without touching the shell.
905
+ *
906
+ * Tag editor: multi-select chips + autocomplete from the wiki's existing tags
907
+ * (GET /dsh-tiddlywiki/tags). Enter/`,` commits the typed value; Backspace on
908
+ * an empty field removes the last chip; the dropdown filters on typing.
909
+ *
814
910
  * @module dsh-tiddlywiki/client/note-widget
815
911
  */
816
912
  const NOTE_ENDPOINT = "/dsh-tiddlywiki/note";
817
913
  const EDIT_ENDPOINT = "/dsh-tiddlywiki/edit";
818
914
  const STATUS_ENDPOINT = "/dsh-tiddlywiki/status";
915
+ const TAGS_ENDPOINT = "/dsh-tiddlywiki/tags";
819
916
  function pad(n) {
820
917
  return n < 10 ? `0${n}` : String(n);
821
918
  }
@@ -823,16 +920,137 @@ function pad(n) {
823
920
  function timestampTitle(date = /* @__PURE__ */ new Date()) {
824
921
  return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}`;
825
922
  }
826
- async function fetchDefaultTag() {
923
+ async function fetchUiOptions() {
924
+ const fallback = {
925
+ showQuickNote: true,
926
+ defaultTag: "inbox"
927
+ };
827
928
  try {
828
929
  const res = await fetch(STATUS_ENDPOINT, { signal: AbortSignal.timeout(5e3) });
829
- if (!res.ok) return "inbox";
830
- return (await res.json()).note?.tag ?? "inbox";
930
+ if (!res.ok) return fallback;
931
+ const payload = await res.json();
932
+ return {
933
+ showQuickNote: payload.ui?.showQuickNote !== false,
934
+ defaultTag: payload.note?.tag ?? "inbox"
935
+ };
831
936
  } catch {
832
- return "inbox";
937
+ return fallback;
833
938
  }
834
939
  }
835
- function mountNoteWidget() {
940
+ /** Multi-tag chip editor with autocomplete from the wiki's existing tags. */
941
+ function buildTagEditor() {
942
+ const wrap = document.createElement("div");
943
+ wrap.className = "dsh-tw-note-tags";
944
+ const chipWrap = document.createElement("div");
945
+ chipWrap.className = "dsh-tw-note-chips";
946
+ const input = document.createElement("input");
947
+ input.className = "dsh-tw-note-taginput";
948
+ input.placeholder = "tag(可多选,自动补全)";
949
+ const suggest = document.createElement("div");
950
+ suggest.className = "dsh-tw-note-tagsuggest";
951
+ suggest.hidden = true;
952
+ wrap.append(chipWrap, input, suggest);
953
+ const chips = [];
954
+ const hideSuggest = () => {
955
+ suggest.hidden = true;
956
+ };
957
+ const renderChips = () => {
958
+ chipWrap.replaceChildren();
959
+ for (const tag of chips) {
960
+ const chip = document.createElement("span");
961
+ chip.className = "dsh-tw-note-tagchip";
962
+ chip.textContent = tag;
963
+ const x = document.createElement("span");
964
+ x.className = "dsh-tw-note-tagchip-x";
965
+ x.textContent = "×";
966
+ x.title = `移除 tag「${tag}」`;
967
+ x.addEventListener("click", (event) => {
968
+ event.stopPropagation();
969
+ const i = chips.indexOf(tag);
970
+ if (i >= 0) {
971
+ chips.splice(i, 1);
972
+ renderChips();
973
+ }
974
+ });
975
+ chip.append(x);
976
+ chipWrap.append(chip);
977
+ }
978
+ };
979
+ const addTag = (tag) => {
980
+ const t = tag.trim();
981
+ if (t.length === 0 || chips.includes(t)) return;
982
+ chips.push(t);
983
+ input.value = "";
984
+ renderChips();
985
+ hideSuggest();
986
+ input.focus();
987
+ };
988
+ const commitInput = () => {
989
+ for (const raw of input.value.split(/\s+/)) addTag(raw);
990
+ };
991
+ let knownTags = [];
992
+ let tagsPromise;
993
+ const ensureTags = () => {
994
+ 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(() => []);
995
+ tagsPromise.then((list) => {
996
+ knownTags = list;
997
+ });
998
+ return tagsPromise;
999
+ };
1000
+ const showSuggest = () => {
1001
+ const q = input.value.trim().toLowerCase();
1002
+ const matches = knownTags.filter((t) => !chips.includes(t) && (q.length === 0 || t.toLowerCase().includes(q))).slice(0, 8);
1003
+ suggest.replaceChildren();
1004
+ for (const tag of matches) {
1005
+ const item = document.createElement("div");
1006
+ item.className = "dsh-tw-note-tagsuggest-item";
1007
+ item.textContent = tag;
1008
+ item.addEventListener("mousedown", (event) => {
1009
+ event.preventDefault();
1010
+ addTag(tag);
1011
+ });
1012
+ suggest.append(item);
1013
+ }
1014
+ suggest.hidden = matches.length === 0;
1015
+ };
1016
+ input.addEventListener("focus", () => {
1017
+ ensureTags().then(showSuggest);
1018
+ });
1019
+ input.addEventListener("input", () => {
1020
+ if (knownTags.length === 0) ensureTags().then(showSuggest);
1021
+ else showSuggest();
1022
+ });
1023
+ input.addEventListener("keydown", (event) => {
1024
+ if (event.key === "Enter" || event.key === ",") {
1025
+ event.preventDefault();
1026
+ commitInput();
1027
+ } else if (event.key === "Backspace" && input.value.length === 0 && chips.length > 0) {
1028
+ chips.pop();
1029
+ renderChips();
1030
+ } else if (event.key === "Escape") hideSuggest();
1031
+ });
1032
+ document.addEventListener("click", (event) => {
1033
+ if (!wrap.contains(event.target)) hideSuggest();
1034
+ }, true);
1035
+ return {
1036
+ el: wrap,
1037
+ getTags: () => {
1038
+ commitInput();
1039
+ return [...chips];
1040
+ },
1041
+ setDefault: (tag) => {
1042
+ if (chips.length === 0) {
1043
+ const t = tag.trim();
1044
+ if (t.length > 0) {
1045
+ chips.push(t);
1046
+ renderChips();
1047
+ }
1048
+ }
1049
+ }
1050
+ };
1051
+ }
1052
+ /** Build the whole widget DOM and wire it up (returns the appended root). */
1053
+ function buildWidget() {
836
1054
  const root = document.createElement("div");
837
1055
  root.className = "dsh-tw-note";
838
1056
  const card = document.createElement("div");
@@ -854,10 +1072,8 @@ function mountNoteWidget() {
854
1072
  const titleInput = document.createElement("input");
855
1073
  titleInput.className = "dsh-tw-note-title";
856
1074
  titleInput.placeholder = "标题(默认时间戳)";
857
- const tagInput = document.createElement("input");
858
- tagInput.className = "dsh-tw-note-tag";
859
- tagInput.placeholder = "tag(默认 inbox)";
860
- fields.append(titleInput, tagInput);
1075
+ const tagEditor = buildTagEditor();
1076
+ fields.append(titleInput, tagEditor.el);
861
1077
  const textarea = document.createElement("textarea");
862
1078
  textarea.className = "dsh-tw-note-text";
863
1079
  textarea.placeholder = "写点东西…(Ctrl+Enter 保存)";
@@ -892,10 +1108,9 @@ function mountNoteWidget() {
892
1108
  card.hidden = false;
893
1109
  opened = true;
894
1110
  resetTitle();
895
- if (tagInput.value.length === 0) tagInput.value = defaultTag;
896
1111
  textarea.focus();
897
- defaultTag = await fetchDefaultTag();
898
- if (tagInput.value.length === 0) tagInput.value = defaultTag;
1112
+ defaultTag = (await fetchUiOptions()).defaultTag;
1113
+ tagEditor.setDefault(defaultTag);
899
1114
  };
900
1115
  const close = () => {
901
1116
  card.hidden = true;
@@ -919,7 +1134,7 @@ function mountNoteWidget() {
919
1134
  headers: { "content-type": "application/json" },
920
1135
  body: JSON.stringify({
921
1136
  title: titleInput.value.trim(),
922
- tag: tagInput.value.trim(),
1137
+ tags: tagEditor.getTags(),
923
1138
  text
924
1139
  }),
925
1140
  signal: AbortSignal.timeout(1e4)
@@ -953,7 +1168,7 @@ function mountNoteWidget() {
953
1168
  const doEdit = async () => {
954
1169
  const title = titleInput.value.trim().length > 0 ? titleInput.value.trim() : timestampTitle();
955
1170
  const text = textarea.value;
956
- const tag = tagInput.value.trim();
1171
+ const tags = tagEditor.getTags();
957
1172
  edit.disabled = true;
958
1173
  edit.textContent = "打开中…";
959
1174
  try {
@@ -962,7 +1177,7 @@ function mountNoteWidget() {
962
1177
  headers: { "content-type": "application/json" },
963
1178
  body: JSON.stringify({
964
1179
  title,
965
- tag,
1180
+ tags,
966
1181
  text
967
1182
  }),
968
1183
  signal: AbortSignal.timeout(1e4)
@@ -988,10 +1203,27 @@ function mountNoteWidget() {
988
1203
  edit.addEventListener("click", () => {
989
1204
  doEdit();
990
1205
  });
991
- return () => {
992
- root.remove();
1206
+ return root;
1207
+ }
1208
+ /**
1209
+ * Mount the floating quick-note widget. Fetches /status first: when
1210
+ * `ui.showQuickNote` is off the widget is never created (no DOM side effects).
1211
+ * Returns a disposer that removes it.
1212
+ */
1213
+ function mountNoteWidget() {
1214
+ let disposed = false;
1215
+ let root;
1216
+ const disposer = () => {
1217
+ disposed = true;
1218
+ root?.remove();
993
1219
  document.querySelector(".dsh-tw-toast")?.remove();
994
1220
  };
1221
+ (async () => {
1222
+ const ui = await fetchUiOptions();
1223
+ if (disposed || !ui.showQuickNote) return;
1224
+ root = buildWidget();
1225
+ })();
1226
+ return disposer;
995
1227
  }
996
1228
  //#endregion
997
1229
  //#region src/client/settings-page.ts
@@ -1109,6 +1341,7 @@ function renderConfigSection(body, config, refresh) {
1109
1341
  section.append(make("h3", "dsh-tw-settings-h", "常规配置"));
1110
1342
  const note = config.note ?? {};
1111
1343
  const git = config.git ?? {};
1344
+ const ui = config.ui ?? {};
1112
1345
  const fields = [];
1113
1346
  const textField = (key, label, initial) => {
1114
1347
  const input = make("input", "dsh-tw-settings-input");
@@ -1159,6 +1392,8 @@ function renderConfigSection(body, config, refresh) {
1159
1392
  numField("git.debounceMs", "自动 commit 防抖(ms)", typeof git.debounceMs === "number" ? git.debounceMs : 6e4);
1160
1393
  textField("git.remote", "git 远端(空=仅本地)", typeof git.remote === "string" ? git.remote : "");
1161
1394
  textField("git.branch", "git 分支", typeof git.branch === "string" ? git.branch : "main");
1395
+ checkField("ui.showQuickNote", "显示「快速笔记」悬浮按钮", ui.showQuickNote !== false);
1396
+ checkField("ui.sidebarLeftCss", "侧边栏移左 CSS(随启动补丁还原被删除的样式)", ui.sidebarLeftCss !== false);
1162
1397
  const save = make("button", "dsh-tw-settings-btn dsh-tw-settings-primary", "保存配置");
1163
1398
  save.type = "button";
1164
1399
  save.addEventListener("click", () => {