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.
@@ -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 {
@@ -74,6 +89,13 @@ html[data-dsh-tw-active] .dshDesktopConversationSurface > :not([data-dsh-tw-view
74
89
  }
75
90
  .dsh-tw-panel-bar button:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,.12)); }
76
91
  .dsh-tw-panel-title { font-weight: 600; margin-right: auto; }
92
+ /* dsh-better-sidebar coexistence: the floating toggle cluster (two 28px
93
+ buttons at the viewport's top-right) lives above the panel (z-index is
94
+ handled in JS — the panel stays below the host layer), so reserve the
95
+ cluster's width at the panel bar's right end — mirroring how
96
+ dsh-better-sidebar pads the DSH session header — and the panel's own
97
+ 状态/重载 buttons never sit underneath it. */
98
+ .dsh-tw-view[data-sidebar-host] .dsh-tw-panel-bar { padding-right: 80px; }
77
99
  .dsh-tw-status-chip { font-variant-numeric: tabular-nums; }
78
100
  .dsh-tw-status-chip[data-state="running"] { color: var(--dsw-alias-state-success-primary, #3eaa5f); }
79
101
  .dsh-tw-status-chip[data-state="starting"] { color: var(--dsw-alias-state-warning-primary, #d9822b); }
@@ -156,6 +178,34 @@ html[data-dsh-tw-active] .dshDesktopConversationSurface > :not([data-dsh-tw-view
156
178
  }
157
179
  .dsh-tw-note-edit:hover:not(:disabled) { filter: brightness(.96); }
158
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)); }
159
209
  /* ── Native-editor popup iframe (quick-note "在 TW 中编辑") ──────────────── */
160
210
  .dsh-tw-editor-popup {
161
211
  position: fixed; left: 0; right: 0; top: 0; bottom: 0; margin: auto;
@@ -429,10 +479,35 @@ const ACTIVATE_EVENT = "dsh-panel-activate";
429
479
  const PANEL_NAME = "dsh-tiddlywiki";
430
480
  /** Overlay z-index: above the shell content, below the note widget (950). */
431
481
  const PANEL_Z_INDEX = 40;
482
+ /**
483
+ * dsh-better-sidebar's unified fixed host layer (its persistent
484
+ * expand/collapse toggle cluster lives inside it, at a global z-index of 25
485
+ * — its internal 45 is trapped by the host's stacking context). When this
486
+ * host is present the panel must stay BELOW it, so the sidebar toggle
487
+ * buttons stay visible and clickable above the full-screen TW panel.
488
+ */
489
+ const PANEL_HOST_SELECTOR = "[data-dsh-panel-host]";
490
+ /** The app's own shell overlay layer (dsh-client-ui-layout pins it at 20). */
491
+ const APP_OVERLAY_Z_INDEX = 20;
432
492
  /** Safety re-measure cadence for shell layout changes CSS can't see. */
433
493
  const SYNC_INTERVAL_MS = 2e3;
434
494
  const STATUS_ENDPOINT$1 = "/dsh-tiddlywiki/status";
435
495
  const RESTART_ENDPOINT$1 = "/dsh-tiddlywiki/restart";
496
+ /**
497
+ * The panel's z-index: below any dsh-better-sidebar host layer so its
498
+ * persistent toggle cluster (top-right corner) stays visible and clickable
499
+ * above the full-screen TW panel, otherwise the default 40. The host's live
500
+ * computed z-index is read instead of hardcoding 25, so the rule tracks
501
+ * plugin updates; the panel is still clamped above the app's own shell
502
+ * overlay layer (dsh-client-ui-layout pins it at 20).
503
+ */
504
+ function resolvePanelZIndex() {
505
+ const host = document.querySelector(PANEL_HOST_SELECTOR);
506
+ if (host === null) return PANEL_Z_INDEX;
507
+ const parsed = parseInt(getComputedStyle(host).zIndex, 10);
508
+ if (!Number.isFinite(parsed)) return PANEL_Z_INDEX;
509
+ return Math.max(APP_OVERLAY_Z_INDEX, Math.min(PANEL_Z_INDEX, parsed - 1));
510
+ }
436
511
  function conversationColumn() {
437
512
  for (const selector of COLUMN_SELECTORS) {
438
513
  const el = document.querySelector(selector);
@@ -526,13 +601,25 @@ function mountPanel(state) {
526
601
  if (container.style.width !== width) container.style.width = width;
527
602
  if (container.style.height !== height) container.style.height = height;
528
603
  };
604
+ /**
605
+ * Coexist with dsh-better-sidebar: keep the panel's z-index below the
606
+ * host layer (so its toggle cluster stays clickable above the panel) and
607
+ * flag the host's presence so CSS can reserve the cluster's width at the
608
+ * panel bar's right end. Re-run whenever the host mounts/unmounts.
609
+ */
610
+ const applyChrome = () => {
611
+ if (container === void 0) return;
612
+ container.style.zIndex = String(resolvePanelZIndex());
613
+ if (document.querySelector(PANEL_HOST_SELECTOR) !== null) container.dataset.sidebarHost = "1";
614
+ else delete container.dataset.sidebarHost;
615
+ };
529
616
  const ensure = () => {
530
617
  if (container !== void 0) return;
531
618
  columnEl = conversationColumn();
532
619
  if (columnEl === void 0) return;
533
620
  container = build();
534
621
  container.style.position = "fixed";
535
- container.style.zIndex = String(PANEL_Z_INDEX);
622
+ applyChrome();
536
623
  document.body.append(container);
537
624
  syncRect();
538
625
  };
@@ -634,6 +721,7 @@ function mountPanel(state) {
634
721
  };
635
722
  const waitObserver = new MutationObserver(() => {
636
723
  ensure();
724
+ applyChrome();
637
725
  });
638
726
  waitObserver.observe(document.body, {
639
727
  childList: true,
@@ -802,14 +890,23 @@ function ensurePopup() {
802
890
  * collapsible, independent of any shell DOM. Lets the human jot drafts /
803
891
  * scratch notes while waiting for the AI or drafting the next prompt.
804
892
  *
805
- * Save posts to /dsh-tiddlywiki/note → an independent tiddler (title & tag
893
+ * Save posts to /dsh-tiddlywiki/note → an independent tiddler (title & tags
806
894
  * editable; defaults: timestamp title + config tag, usually "inbox").
807
895
  *
896
+ * ui.showQuickNote config: the widget is mounted async and stays hidden (and
897
+ * never appended) when the option is off, so the toggle button can be disabled
898
+ * from the settings page without touching the shell.
899
+ *
900
+ * Tag editor: multi-select chips + autocomplete from the wiki's existing tags
901
+ * (GET /dsh-tiddlywiki/tags). Enter/`,` commits the typed value; Backspace on
902
+ * an empty field removes the last chip; the dropdown filters on typing.
903
+ *
808
904
  * @module dsh-tiddlywiki/client/note-widget
809
905
  */
810
906
  const NOTE_ENDPOINT = "/dsh-tiddlywiki/note";
811
907
  const EDIT_ENDPOINT = "/dsh-tiddlywiki/edit";
812
908
  const STATUS_ENDPOINT = "/dsh-tiddlywiki/status";
909
+ const TAGS_ENDPOINT = "/dsh-tiddlywiki/tags";
813
910
  function pad(n) {
814
911
  return n < 10 ? `0${n}` : String(n);
815
912
  }
@@ -817,16 +914,137 @@ function pad(n) {
817
914
  function timestampTitle(date = /* @__PURE__ */ new Date()) {
818
915
  return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}`;
819
916
  }
820
- async function fetchDefaultTag() {
917
+ async function fetchUiOptions() {
918
+ const fallback = {
919
+ showQuickNote: true,
920
+ defaultTag: "inbox"
921
+ };
821
922
  try {
822
923
  const res = await fetch(STATUS_ENDPOINT, { signal: AbortSignal.timeout(5e3) });
823
- if (!res.ok) return "inbox";
824
- return (await res.json()).note?.tag ?? "inbox";
924
+ if (!res.ok) return fallback;
925
+ const payload = await res.json();
926
+ return {
927
+ showQuickNote: payload.ui?.showQuickNote !== false,
928
+ defaultTag: payload.note?.tag ?? "inbox"
929
+ };
825
930
  } catch {
826
- return "inbox";
931
+ return fallback;
827
932
  }
828
933
  }
829
- function mountNoteWidget() {
934
+ /** Multi-tag chip editor with autocomplete from the wiki's existing tags. */
935
+ function buildTagEditor() {
936
+ const wrap = document.createElement("div");
937
+ wrap.className = "dsh-tw-note-tags";
938
+ const chipWrap = document.createElement("div");
939
+ chipWrap.className = "dsh-tw-note-chips";
940
+ const input = document.createElement("input");
941
+ input.className = "dsh-tw-note-taginput";
942
+ input.placeholder = "tag(可多选,自动补全)";
943
+ const suggest = document.createElement("div");
944
+ suggest.className = "dsh-tw-note-tagsuggest";
945
+ suggest.hidden = true;
946
+ wrap.append(chipWrap, input, suggest);
947
+ const chips = [];
948
+ const hideSuggest = () => {
949
+ suggest.hidden = true;
950
+ };
951
+ const renderChips = () => {
952
+ chipWrap.replaceChildren();
953
+ for (const tag of chips) {
954
+ const chip = document.createElement("span");
955
+ chip.className = "dsh-tw-note-tagchip";
956
+ chip.textContent = tag;
957
+ const x = document.createElement("span");
958
+ x.className = "dsh-tw-note-tagchip-x";
959
+ x.textContent = "×";
960
+ x.title = `移除 tag「${tag}」`;
961
+ x.addEventListener("click", (event) => {
962
+ event.stopPropagation();
963
+ const i = chips.indexOf(tag);
964
+ if (i >= 0) {
965
+ chips.splice(i, 1);
966
+ renderChips();
967
+ }
968
+ });
969
+ chip.append(x);
970
+ chipWrap.append(chip);
971
+ }
972
+ };
973
+ const addTag = (tag) => {
974
+ const t = tag.trim();
975
+ if (t.length === 0 || chips.includes(t)) return;
976
+ chips.push(t);
977
+ input.value = "";
978
+ renderChips();
979
+ hideSuggest();
980
+ input.focus();
981
+ };
982
+ const commitInput = () => {
983
+ for (const raw of input.value.split(/\s+/)) addTag(raw);
984
+ };
985
+ let knownTags = [];
986
+ let tagsPromise;
987
+ const ensureTags = () => {
988
+ 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(() => []);
989
+ tagsPromise.then((list) => {
990
+ knownTags = list;
991
+ });
992
+ return tagsPromise;
993
+ };
994
+ const showSuggest = () => {
995
+ const q = input.value.trim().toLowerCase();
996
+ const matches = knownTags.filter((t) => !chips.includes(t) && (q.length === 0 || t.toLowerCase().includes(q))).slice(0, 8);
997
+ suggest.replaceChildren();
998
+ for (const tag of matches) {
999
+ const item = document.createElement("div");
1000
+ item.className = "dsh-tw-note-tagsuggest-item";
1001
+ item.textContent = tag;
1002
+ item.addEventListener("mousedown", (event) => {
1003
+ event.preventDefault();
1004
+ addTag(tag);
1005
+ });
1006
+ suggest.append(item);
1007
+ }
1008
+ suggest.hidden = matches.length === 0;
1009
+ };
1010
+ input.addEventListener("focus", () => {
1011
+ ensureTags().then(showSuggest);
1012
+ });
1013
+ input.addEventListener("input", () => {
1014
+ if (knownTags.length === 0) ensureTags().then(showSuggest);
1015
+ else showSuggest();
1016
+ });
1017
+ input.addEventListener("keydown", (event) => {
1018
+ if (event.key === "Enter" || event.key === ",") {
1019
+ event.preventDefault();
1020
+ commitInput();
1021
+ } else if (event.key === "Backspace" && input.value.length === 0 && chips.length > 0) {
1022
+ chips.pop();
1023
+ renderChips();
1024
+ } else if (event.key === "Escape") hideSuggest();
1025
+ });
1026
+ document.addEventListener("click", (event) => {
1027
+ if (!wrap.contains(event.target)) hideSuggest();
1028
+ }, true);
1029
+ return {
1030
+ el: wrap,
1031
+ getTags: () => {
1032
+ commitInput();
1033
+ return [...chips];
1034
+ },
1035
+ setDefault: (tag) => {
1036
+ if (chips.length === 0) {
1037
+ const t = tag.trim();
1038
+ if (t.length > 0) {
1039
+ chips.push(t);
1040
+ renderChips();
1041
+ }
1042
+ }
1043
+ }
1044
+ };
1045
+ }
1046
+ /** Build the whole widget DOM and wire it up (returns the appended root). */
1047
+ function buildWidget() {
830
1048
  const root = document.createElement("div");
831
1049
  root.className = "dsh-tw-note";
832
1050
  const card = document.createElement("div");
@@ -848,10 +1066,8 @@ function mountNoteWidget() {
848
1066
  const titleInput = document.createElement("input");
849
1067
  titleInput.className = "dsh-tw-note-title";
850
1068
  titleInput.placeholder = "标题(默认时间戳)";
851
- const tagInput = document.createElement("input");
852
- tagInput.className = "dsh-tw-note-tag";
853
- tagInput.placeholder = "tag(默认 inbox)";
854
- fields.append(titleInput, tagInput);
1069
+ const tagEditor = buildTagEditor();
1070
+ fields.append(titleInput, tagEditor.el);
855
1071
  const textarea = document.createElement("textarea");
856
1072
  textarea.className = "dsh-tw-note-text";
857
1073
  textarea.placeholder = "写点东西…(Ctrl+Enter 保存)";
@@ -886,10 +1102,9 @@ function mountNoteWidget() {
886
1102
  card.hidden = false;
887
1103
  opened = true;
888
1104
  resetTitle();
889
- if (tagInput.value.length === 0) tagInput.value = defaultTag;
890
1105
  textarea.focus();
891
- defaultTag = await fetchDefaultTag();
892
- if (tagInput.value.length === 0) tagInput.value = defaultTag;
1106
+ defaultTag = (await fetchUiOptions()).defaultTag;
1107
+ tagEditor.setDefault(defaultTag);
893
1108
  };
894
1109
  const close = () => {
895
1110
  card.hidden = true;
@@ -913,7 +1128,7 @@ function mountNoteWidget() {
913
1128
  headers: { "content-type": "application/json" },
914
1129
  body: JSON.stringify({
915
1130
  title: titleInput.value.trim(),
916
- tag: tagInput.value.trim(),
1131
+ tags: tagEditor.getTags(),
917
1132
  text
918
1133
  }),
919
1134
  signal: AbortSignal.timeout(1e4)
@@ -947,7 +1162,7 @@ function mountNoteWidget() {
947
1162
  const doEdit = async () => {
948
1163
  const title = titleInput.value.trim().length > 0 ? titleInput.value.trim() : timestampTitle();
949
1164
  const text = textarea.value;
950
- const tag = tagInput.value.trim();
1165
+ const tags = tagEditor.getTags();
951
1166
  edit.disabled = true;
952
1167
  edit.textContent = "打开中…";
953
1168
  try {
@@ -956,7 +1171,7 @@ function mountNoteWidget() {
956
1171
  headers: { "content-type": "application/json" },
957
1172
  body: JSON.stringify({
958
1173
  title,
959
- tag,
1174
+ tags,
960
1175
  text
961
1176
  }),
962
1177
  signal: AbortSignal.timeout(1e4)
@@ -982,10 +1197,27 @@ function mountNoteWidget() {
982
1197
  edit.addEventListener("click", () => {
983
1198
  doEdit();
984
1199
  });
985
- return () => {
986
- root.remove();
1200
+ return root;
1201
+ }
1202
+ /**
1203
+ * Mount the floating quick-note widget. Fetches /status first: when
1204
+ * `ui.showQuickNote` is off the widget is never created (no DOM side effects).
1205
+ * Returns a disposer that removes it.
1206
+ */
1207
+ function mountNoteWidget() {
1208
+ let disposed = false;
1209
+ let root;
1210
+ const disposer = () => {
1211
+ disposed = true;
1212
+ root?.remove();
987
1213
  document.querySelector(".dsh-tw-toast")?.remove();
988
1214
  };
1215
+ (async () => {
1216
+ const ui = await fetchUiOptions();
1217
+ if (disposed || !ui.showQuickNote) return;
1218
+ root = buildWidget();
1219
+ })();
1220
+ return disposer;
989
1221
  }
990
1222
  //#endregion
991
1223
  //#region src/client/settings-page.ts
@@ -1103,6 +1335,7 @@ function renderConfigSection(body, config, refresh) {
1103
1335
  section.append(make("h3", "dsh-tw-settings-h", "常规配置"));
1104
1336
  const note = config.note ?? {};
1105
1337
  const git = config.git ?? {};
1338
+ const ui = config.ui ?? {};
1106
1339
  const fields = [];
1107
1340
  const textField = (key, label, initial) => {
1108
1341
  const input = make("input", "dsh-tw-settings-input");
@@ -1153,6 +1386,8 @@ function renderConfigSection(body, config, refresh) {
1153
1386
  numField("git.debounceMs", "自动 commit 防抖(ms)", typeof git.debounceMs === "number" ? git.debounceMs : 6e4);
1154
1387
  textField("git.remote", "git 远端(空=仅本地)", typeof git.remote === "string" ? git.remote : "");
1155
1388
  textField("git.branch", "git 分支", typeof git.branch === "string" ? git.branch : "main");
1389
+ checkField("ui.showQuickNote", "显示「快速笔记」悬浮按钮", ui.showQuickNote !== false);
1390
+ checkField("ui.sidebarLeftCss", "侧边栏移左 CSS(随启动补丁还原被删除的样式)", ui.sidebarLeftCss !== false);
1156
1391
  const save = make("button", "dsh-tw-settings-btn dsh-tw-settings-primary", "保存配置");
1157
1392
  save.type = "button";
1158
1393
  save.addEventListener("click", () => {