dsh-tiddlywiki 0.1.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/LICENSE +21 -0
- package/README.md +172 -0
- package/cordis.patch.yml +13 -0
- package/lib/client.bundle.js +1417 -0
- package/lib/client.js +1427 -0
- package/lib/index.js +2153 -0
- package/lib/index.js.map +1 -0
- package/package.json +63 -0
- package/src/client/editor-popup.ts +121 -0
- package/src/client/index.ts +76 -0
- package/src/client/note-widget.ts +210 -0
- package/src/client/panel.ts +304 -0
- package/src/client/settings-page.ts +397 -0
- package/src/client/sidebar-entry.ts +148 -0
- package/src/client/state.ts +39 -0
- package/src/client/styles.ts +235 -0
- package/src/client/toast.ts +22 -0
- package/src/host/admin.ts +408 -0
- package/src/host/config.ts +86 -0
- package/src/host/git.ts +218 -0
- package/src/host/routes.ts +233 -0
- package/src/host/seed-notes.ts +62 -0
- package/src/host/tools.ts +254 -0
- package/src/host/tw-api.ts +157 -0
- package/src/host/wiki.ts +287 -0
- package/src/index.ts +331 -0
- package/src/sdk.ts +198 -0
|
@@ -0,0 +1,1417 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
//#region \0rolldown/runtime.js
|
|
3
|
+
var __create = Object.create;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
8
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __copyProps = (to, from, except, desc) => {
|
|
10
|
+
if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
|
|
11
|
+
key = keys[i];
|
|
12
|
+
if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
|
|
13
|
+
get: ((k) => from[k]).bind(null, key),
|
|
14
|
+
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
return to;
|
|
18
|
+
};
|
|
19
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
|
|
20
|
+
value: mod,
|
|
21
|
+
enumerable: true
|
|
22
|
+
}) : target, mod));
|
|
23
|
+
//#endregion
|
|
24
|
+
let react = require("react");
|
|
25
|
+
react = __toESM(react, 1);
|
|
26
|
+
//#region src/client/styles.ts
|
|
27
|
+
/**
|
|
28
|
+
* Client stylesheet injection (design doc D5 — pure DOM, no React).
|
|
29
|
+
* All rules are `dsh-tw-*` scoped; the tag is stable per plugin so the HMR
|
|
30
|
+
* driver can identify it on rebuild.
|
|
31
|
+
*
|
|
32
|
+
* Theme: uses the live `--dsw-alias-*` design tokens (same family as the
|
|
33
|
+
* shutdown launcher) with fallbacks, so it follows light/dark automatically.
|
|
34
|
+
*
|
|
35
|
+
* @module dsh-tiddlywiki/client/styles
|
|
36
|
+
*/
|
|
37
|
+
const STYLE_ID = "dsh-tiddlywiki-styles";
|
|
38
|
+
const CSS_TEXT = `
|
|
39
|
+
/* ── sidebar entry ───────────────────────────────────────────── */
|
|
40
|
+
.dsh-tw-entry {
|
|
41
|
+
display: flex; align-items: center; gap: 8px; position: relative;
|
|
42
|
+
width: calc(100% - 8px); margin: 2px 4px; padding: 6px 10px;
|
|
43
|
+
border: none; border-radius: 8px; background: transparent;
|
|
44
|
+
color: var(--dsw-alias-label-secondary, inherit); font: inherit; font-size: 13px;
|
|
45
|
+
cursor: pointer; text-align: left;
|
|
46
|
+
}
|
|
47
|
+
.dsh-tw-entry:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,.12)); color: var(--dsw-alias-label-primary, inherit); }
|
|
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
|
+
.dsh-tw-entry svg { flex: none; }
|
|
50
|
+
.dsh-tw-entry .dsh-tw-entry-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
51
|
+
|
|
52
|
+
/* ── center-column panel (fixed overlay, JS-pinned to the column rect) ── */
|
|
53
|
+
.dsh-tw-view {
|
|
54
|
+
display: none; flex-direction: column; min-height: 0; box-sizing: border-box;
|
|
55
|
+
background: var(--dsw-alias-bg-layer-1, var(--dsw-bg, #fff));
|
|
56
|
+
}
|
|
57
|
+
html[data-dsh-tw-active] .dsh-tw-view { display: flex; }
|
|
58
|
+
/* Hide the conversation content the panel overlays (all three column gens). */
|
|
59
|
+
html[data-dsh-tw-active] [data-pane="conversation"] > :not([data-dsh-tw-view]),
|
|
60
|
+
html[data-dsh-tw-active] [class*="centerCol"] > :not([data-dsh-tw-view]),
|
|
61
|
+
html[data-dsh-tw-active] .dshDesktopConversationSurface > :not([data-dsh-tw-view]) { display: none !important; }
|
|
62
|
+
|
|
63
|
+
.dsh-tw-panel-bar {
|
|
64
|
+
display: flex; align-items: center; gap: 8px; padding: 6px 10px;
|
|
65
|
+
border-bottom: 1px solid var(--dsw-alias-border-l2, rgba(128,128,128,.18));
|
|
66
|
+
background: var(--dsw-alias-bg-layer-1, transparent);
|
|
67
|
+
font-size: 12px; color: var(--dsw-alias-label-secondary, inherit);
|
|
68
|
+
flex: none;
|
|
69
|
+
}
|
|
70
|
+
.dsh-tw-panel-bar button {
|
|
71
|
+
border: 1px solid var(--dsw-alias-border-l2, rgba(128,128,128,.25));
|
|
72
|
+
background: transparent; color: inherit; font: inherit; font-size: 12px;
|
|
73
|
+
padding: 2px 8px; border-radius: 6px; cursor: pointer;
|
|
74
|
+
}
|
|
75
|
+
.dsh-tw-panel-bar button:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,.12)); }
|
|
76
|
+
.dsh-tw-panel-title { font-weight: 600; margin-right: auto; }
|
|
77
|
+
.dsh-tw-status-chip { font-variant-numeric: tabular-nums; }
|
|
78
|
+
.dsh-tw-status-chip[data-state="running"] { color: var(--dsw-alias-state-success-primary, #3eaa5f); }
|
|
79
|
+
.dsh-tw-status-chip[data-state="starting"] { color: var(--dsw-alias-state-warning-primary, #d9822b); }
|
|
80
|
+
.dsh-tw-status-chip[data-state="failed"], .dsh-tw-status-chip[data-state="stopped"] { color: var(--dsw-alias-state-error-primary, #d13b3b); }
|
|
81
|
+
|
|
82
|
+
.dsh-tw-panel-frame { flex: 1; min-height: 0; border: 0; width: 100%; display: block; background: #fff; }
|
|
83
|
+
/* [hidden] must beat the author display rules above (UA hidden is overridden). */
|
|
84
|
+
.dsh-tw-panel-frame[hidden],
|
|
85
|
+
.dsh-tw-panel-error[hidden] { display: none !important; }
|
|
86
|
+
|
|
87
|
+
.dsh-tw-panel-error {
|
|
88
|
+
flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center;
|
|
89
|
+
gap: 10px; color: var(--dsw-alias-label-secondary, #666); font-size: 13px; text-align: center; padding: 20px;
|
|
90
|
+
}
|
|
91
|
+
.dsh-tw-panel-error button {
|
|
92
|
+
border: 1px solid var(--dsw-alias-border-l2, rgba(128,128,128,.25)); background: transparent;
|
|
93
|
+
color: inherit; font: inherit; padding: 6px 14px; border-radius: 8px; cursor: pointer;
|
|
94
|
+
}
|
|
95
|
+
.dsh-tw-panel-error button:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,.12)); }
|
|
96
|
+
.dsh-tw-panel-error code { font-size: 11px; opacity: .8; max-width: 80%; overflow-wrap: anywhere; }
|
|
97
|
+
|
|
98
|
+
/* ── floating quick-note widget ────────────────────────────────
|
|
99
|
+
Positioned ABOVE the shutdown launcher FAB (fixed right:24 bottom:24,
|
|
100
|
+
z-index 900, 46px) and BELOW its confirm overlay (z-index 1000). */
|
|
101
|
+
.dsh-tw-note {
|
|
102
|
+
position: fixed; right: 24px; bottom: 88px; z-index: 950;
|
|
103
|
+
display: flex; flex-direction: column; align-items: flex-end; gap: 10px;
|
|
104
|
+
font-family: inherit;
|
|
105
|
+
}
|
|
106
|
+
.dsh-tw-note-card {
|
|
107
|
+
width: 340px; max-width: calc(100vw - 40px);
|
|
108
|
+
background: var(--dsw-alias-bg-layer-2, #fff); color: var(--dsw-alias-label-primary, #222);
|
|
109
|
+
border: 1px solid var(--dsw-alias-border-l2, rgba(0,0,0,.15));
|
|
110
|
+
border-radius: 14px; box-shadow: var(--dsw-shadow-lv3, 0 8px 30px rgba(0,0,0,.22));
|
|
111
|
+
padding: 12px; display: flex; flex-direction: column; gap: 10px;
|
|
112
|
+
font-size: 13px;
|
|
113
|
+
}
|
|
114
|
+
.dsh-tw-note-card[hidden] { display: none; }
|
|
115
|
+
.dsh-tw-note-head {
|
|
116
|
+
display: flex; align-items: center; gap: 8px;
|
|
117
|
+
font-size: 12px; font-weight: 600; color: var(--dsw-alias-label-primary, #222);
|
|
118
|
+
}
|
|
119
|
+
.dsh-tw-note-head .dsh-tw-note-label { margin-right: auto; display: flex; align-items: center; gap: 6px; }
|
|
120
|
+
.dsh-tw-note-close {
|
|
121
|
+
border: none; background: transparent; color: var(--dsw-alias-label-secondary, #888);
|
|
122
|
+
font: inherit; font-size: 15px; line-height: 1; padding: 2px 6px; border-radius: 6px; cursor: pointer;
|
|
123
|
+
}
|
|
124
|
+
.dsh-tw-note-close:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,.12)); color: var(--dsw-alias-label-primary, #222); }
|
|
125
|
+
.dsh-tw-note-fields { display: flex; gap: 8px; }
|
|
126
|
+
.dsh-tw-note-fields input {
|
|
127
|
+
flex: 1; min-width: 0; border: 1px solid var(--dsw-alias-border-l2, rgba(0,0,0,.18));
|
|
128
|
+
border-radius: 8px; padding: 6px 9px; font: inherit; font-size: 12px;
|
|
129
|
+
background: var(--dsw-alias-bg-layer-1, #fff); color: var(--dsw-alias-label-primary, inherit);
|
|
130
|
+
transition: border-color 120ms ease, box-shadow 120ms ease;
|
|
131
|
+
}
|
|
132
|
+
.dsh-tw-note-fields input:focus, .dsh-tw-note-text:focus {
|
|
133
|
+
outline: none; border-color: var(--dsw-alias-brand-primary, #3e63dd);
|
|
134
|
+
box-shadow: 0 0 0 2px color-mix(in srgb, var(--dsw-alias-brand-primary, #3e63dd) 25%, transparent);
|
|
135
|
+
}
|
|
136
|
+
.dsh-tw-note-text {
|
|
137
|
+
border: 1px solid var(--dsw-alias-border-l2, rgba(0,0,0,.18)); border-radius: 8px;
|
|
138
|
+
padding: 8px 9px; font: inherit; font-size: 13px; min-height: 120px; resize: vertical;
|
|
139
|
+
background: var(--dsw-alias-bg-layer-1, #fff); color: var(--dsw-alias-label-primary, inherit);
|
|
140
|
+
transition: border-color 120ms ease, box-shadow 120ms ease;
|
|
141
|
+
}
|
|
142
|
+
.dsh-tw-note-foot { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
|
|
143
|
+
.dsh-tw-note-hint { font-size: 11px; color: var(--dsw-alias-label-dimmed, #999); }
|
|
144
|
+
.dsh-tw-note-save {
|
|
145
|
+
border: 1px solid transparent; background: var(--dsw-alias-brand-primary, #3e63dd); color: #fff;
|
|
146
|
+
font: inherit; font-size: 12px; padding: 6px 16px; border-radius: 8px; cursor: pointer;
|
|
147
|
+
transition: filter 120ms ease;
|
|
148
|
+
}
|
|
149
|
+
.dsh-tw-note-save:hover:not(:disabled) { filter: brightness(1.08); }
|
|
150
|
+
.dsh-tw-note-save:disabled { opacity: .55; cursor: default; }
|
|
151
|
+
.dsh-tw-note-edit {
|
|
152
|
+
border: 1px solid var(--dsw-alias-border-l2, rgba(0,0,0,.18));
|
|
153
|
+
background: var(--dsw-alias-bg-layer-2, #fff); color: var(--dsw-alias-label-primary, #222);
|
|
154
|
+
font: inherit; font-size: 12px; padding: 6px 10px; border-radius: 8px; cursor: pointer;
|
|
155
|
+
transition: filter 120ms ease;
|
|
156
|
+
}
|
|
157
|
+
.dsh-tw-note-edit:hover:not(:disabled) { filter: brightness(.96); }
|
|
158
|
+
.dsh-tw-note-edit:disabled { opacity: .55; cursor: default; }
|
|
159
|
+
/* ── Native-editor popup iframe (quick-note "在 TW 中编辑") ──────────────── */
|
|
160
|
+
.dsh-tw-editor-popup {
|
|
161
|
+
position: fixed; left: 0; right: 0; top: 0; bottom: 0; margin: auto;
|
|
162
|
+
width: min(880px, 92vw); height: min(640px, 86vh);
|
|
163
|
+
display: flex; flex-direction: column; overflow: hidden;
|
|
164
|
+
background: var(--dsw-alias-bg-layer-2, #fff); color: var(--dsw-alias-label-primary, #222);
|
|
165
|
+
border: 1px solid var(--dsw-alias-border-l2, rgba(0,0,0,.2));
|
|
166
|
+
border-radius: 12px; box-shadow: 0 14px 44px rgba(0,0,0,.3);
|
|
167
|
+
z-index: 980; font-family: inherit; font-size: 13px;
|
|
168
|
+
}
|
|
169
|
+
.dsh-tw-editor-bar {
|
|
170
|
+
display: flex; align-items: center; gap: 8px; flex: 0 0 auto;
|
|
171
|
+
padding: 7px 8px 7px 14px; cursor: move; user-select: none;
|
|
172
|
+
background: var(--dsw-alias-bg-layer-1, #f4f5f7);
|
|
173
|
+
border-bottom: 1px solid var(--dsw-alias-border-l1, rgba(0,0,0,.1));
|
|
174
|
+
}
|
|
175
|
+
.dsh-tw-editor-title { flex: 1; min-width: 0; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
176
|
+
.dsh-tw-editor-close {
|
|
177
|
+
border: none; background: transparent; cursor: pointer; font-size: 14px; line-height: 1;
|
|
178
|
+
color: var(--dsw-alias-label-dimmed, #888); padding: 5px 8px; border-radius: 6px;
|
|
179
|
+
}
|
|
180
|
+
.dsh-tw-editor-close:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,.12)); color: var(--dsw-alias-label-primary, #222); }
|
|
181
|
+
.dsh-tw-editor-frame { flex: 1; min-height: 0; width: 100%; border: 0; background: #fff; }
|
|
182
|
+
.dsh-tw-editor-resize { position: absolute; right: 0; bottom: 0; width: 16px; height: 16px; cursor: nwse-resize; }
|
|
183
|
+
.dsh-tw-note-toggle {
|
|
184
|
+
border: 1px solid var(--dsw-alias-border-l2, rgba(0,0,0,.18));
|
|
185
|
+
background: var(--dsw-alias-bg-layer-2, #fff); color: var(--dsw-alias-label-primary, #222);
|
|
186
|
+
font: inherit; font-size: 13px; padding: 9px 16px; border-radius: 999px;
|
|
187
|
+
box-shadow: var(--dsw-shadow-lv3, 0 4px 16px rgba(0,0,0,.16)); cursor: pointer;
|
|
188
|
+
display: inline-flex; align-items: center; gap: 6px;
|
|
189
|
+
transition: background-color 120ms ease;
|
|
190
|
+
}
|
|
191
|
+
.dsh-tw-note-toggle:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,.1)); }
|
|
192
|
+
|
|
193
|
+
/* ── toast ──────────────────────────────────────────────────── */
|
|
194
|
+
.dsh-tw-toast {
|
|
195
|
+
position: fixed; left: 50%; bottom: 96px; transform: translateX(-50%);
|
|
196
|
+
background: var(--dsw-alias-bg-inverse, rgba(30,30,30,.92)); color: var(--dsw-alias-label-inverse, #fff);
|
|
197
|
+
font-size: 13px; padding: 8px 16px; border-radius: 999px; z-index: 10001;
|
|
198
|
+
opacity: 0; transition: opacity .2s ease; pointer-events: none;
|
|
199
|
+
max-width: 80vw; overflow-wrap: anywhere;
|
|
200
|
+
}
|
|
201
|
+
.dsh-tw-toast.dsh-tw-toast-show { opacity: 1; }
|
|
202
|
+
|
|
203
|
+
/* ── settings page (config panel §13) ────────────────────────── */
|
|
204
|
+
.dsh-tw-settings { display: flex; flex-direction: column; gap: 10px; padding: 12px 16px; min-width: 0; }
|
|
205
|
+
.dsh-tw-settings-row { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
|
|
206
|
+
.dsh-tw-settings-chip {
|
|
207
|
+
font-size: 12px; padding: 2px 10px; border-radius: 999px;
|
|
208
|
+
border: 1px solid var(--dsw-alias-border-l2, rgba(128,128,128,.25));
|
|
209
|
+
}
|
|
210
|
+
.dsh-tw-settings-chip[data-state="running"] { color: var(--dsw-alias-state-success-primary, #3eaa5f); }
|
|
211
|
+
.dsh-tw-settings-chip[data-state="starting"] { color: var(--dsw-alias-state-warning-primary, #d9822b); }
|
|
212
|
+
.dsh-tw-settings-chip[data-state="failed"], .dsh-tw-settings-chip[data-state="stopped"] { color: var(--dsw-alias-state-error-primary, #d13b3b); }
|
|
213
|
+
.dsh-tw-settings-section { display: flex; flex-direction: column; gap: 8px; padding: 10px 12px; border-radius: 10px; border: 1px solid var(--dsw-alias-border-l2, rgba(128,128,128,.18)); }
|
|
214
|
+
.dsh-tw-settings-h { margin: 0; font-size: 13px; font-weight: 600; color: var(--dsw-alias-label-primary, #222); }
|
|
215
|
+
.dsh-tw-settings-field { display: flex; align-items: center; gap: 8px; font-size: 12px; color: var(--dsw-alias-label-secondary, inherit); }
|
|
216
|
+
.dsh-tw-settings-field-check { cursor: pointer; }
|
|
217
|
+
.dsh-tw-settings-label { flex: 0 0 170px; }
|
|
218
|
+
.dsh-tw-settings-input {
|
|
219
|
+
flex: 1; min-width: 0; font: inherit; font-size: 12px; padding: 4px 8px; border-radius: 6px;
|
|
220
|
+
border: 1px solid var(--dsw-alias-border-l2, rgba(128,128,128,.28));
|
|
221
|
+
background: var(--dsw-alias-bg-input, transparent); color: var(--dsw-alias-label-primary, #222);
|
|
222
|
+
}
|
|
223
|
+
.dsh-tw-settings-input:focus {
|
|
224
|
+
outline: none; border-color: var(--dsw-alias-brand-primary, #3e63dd);
|
|
225
|
+
box-shadow: 0 0 0 3px color-mix(in srgb, var(--dsw-alias-brand-primary, #3e63dd) 22%, transparent);
|
|
226
|
+
}
|
|
227
|
+
.dsh-tw-settings-list { display: flex; flex-direction: column; gap: 2px; max-height: 240px; overflow: auto; }
|
|
228
|
+
.dsh-tw-settings-plugin { display: flex; align-items: center; gap: 8px; padding: 3px 4px; border-radius: 6px; font-size: 12px; }
|
|
229
|
+
.dsh-tw-settings-plugin:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,.08)); }
|
|
230
|
+
.dsh-tw-settings-name { font-weight: 500; flex: 0 0 130px; color: var(--dsw-alias-label-primary, #222); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
231
|
+
.dsh-tw-settings-head { padding: 2px 4px 4px; font-size: 11px; }
|
|
232
|
+
.dsh-tw-settings-col { flex: 0 0 14px; text-align: center; color: var(--dsw-alias-label-dimmed, #999); }
|
|
233
|
+
.dsh-tw-settings-row.dsh-tw-settings-plugin input { flex: 0 0 auto; margin: 0; }
|
|
234
|
+
.dsh-tw-settings-muted { color: var(--dsw-alias-label-dimmed, #999); font-size: 12px; }
|
|
235
|
+
.dsh-tw-settings-btn {
|
|
236
|
+
align-self: flex-start; border: 1px solid var(--dsw-alias-border-l2, rgba(128,128,128,.28));
|
|
237
|
+
background: transparent; color: var(--dsw-alias-label-primary, #222);
|
|
238
|
+
font: inherit; font-size: 12px; padding: 4px 12px; border-radius: 7px; cursor: pointer;
|
|
239
|
+
}
|
|
240
|
+
.dsh-tw-settings-btn:hover:not(:disabled) { background: var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,.1)); }
|
|
241
|
+
.dsh-tw-settings-btn:disabled { opacity: .55; cursor: default; }
|
|
242
|
+
.dsh-tw-settings-btn.dsh-tw-settings-primary {
|
|
243
|
+
background: var(--dsw-alias-brand-primary, #3e63dd); border-color: transparent; color: #fff;
|
|
244
|
+
}
|
|
245
|
+
.dsh-tw-settings-error { color: var(--dsw-alias-state-error-primary, #d13b3b); font-size: 12px; }
|
|
246
|
+
.dsh-tw-settings-search { flex: 0 0 auto; max-width: 220px; }
|
|
247
|
+
.dsh-tw-settings-check { accent-color: var(--dsw-alias-brand-primary, #3e63dd); }
|
|
248
|
+
`;
|
|
249
|
+
function injectStyles() {
|
|
250
|
+
if (typeof document === "undefined") return;
|
|
251
|
+
let el = document.getElementById(STYLE_ID);
|
|
252
|
+
if (el !== null) return;
|
|
253
|
+
el = document.createElement("style");
|
|
254
|
+
el.id = STYLE_ID;
|
|
255
|
+
el.dataset.plugin = "dsh-tiddlywiki";
|
|
256
|
+
el.textContent = CSS_TEXT;
|
|
257
|
+
document.head.append(el);
|
|
258
|
+
}
|
|
259
|
+
//#endregion
|
|
260
|
+
//#region src/client/state.ts
|
|
261
|
+
var PanelState = class {
|
|
262
|
+
open = false;
|
|
263
|
+
listeners = /* @__PURE__ */ new Set();
|
|
264
|
+
isOpen() {
|
|
265
|
+
return this.open;
|
|
266
|
+
}
|
|
267
|
+
toggle() {
|
|
268
|
+
this.set(!this.open);
|
|
269
|
+
}
|
|
270
|
+
openPanel() {
|
|
271
|
+
this.set(true);
|
|
272
|
+
}
|
|
273
|
+
closePanel() {
|
|
274
|
+
this.set(false);
|
|
275
|
+
}
|
|
276
|
+
set(value) {
|
|
277
|
+
if (this.open === value) return;
|
|
278
|
+
this.open = value;
|
|
279
|
+
for (const listener of [...this.listeners]) listener(value);
|
|
280
|
+
}
|
|
281
|
+
subscribe(listener) {
|
|
282
|
+
this.listeners.add(listener);
|
|
283
|
+
return () => {
|
|
284
|
+
this.listeners.delete(listener);
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
};
|
|
288
|
+
/** Inline icon: a wiki page with a TiddlyWiki-style "T" (nav-icon look). */
|
|
289
|
+
const ICON = "<svg viewBox=\"0 0 16 16\" width=\"14\" height=\"14\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.3\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"M4 2.5h8a1 1 0 0 1 1 1v9a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1v-9a1 1 0 0 1 1-1z\"/><path d=\"M6 6h4M6 8.5h2.5\"/></svg>";
|
|
290
|
+
/** Family entries from sibling plugins, kept in a stable relative order. */
|
|
291
|
+
const FAMILY_SELECTOR = "[data-dsh-tw-entry], [data-dsh-atb-entry], [data-dsh-taskboard-entry], [data-dsh-ssh-entry]";
|
|
292
|
+
/** Find the sidebar shell root element, or undefined while not yet mounted. */
|
|
293
|
+
function sidebarRoot() {
|
|
294
|
+
const column = document.querySelector("[data-pane=\"sidebar\"], [class*=\"sidebarCol\"], .dshDesktopUpstreamSidebar, .dshDesktopSidebarSurface");
|
|
295
|
+
if (column === null) return void 0;
|
|
296
|
+
return column.querySelector("[class*=\"logoRow\"]")?.parentElement ?? column.firstElementChild;
|
|
297
|
+
}
|
|
298
|
+
/** The New Session button inside the sidebar root. */
|
|
299
|
+
function newSessionButton(root) {
|
|
300
|
+
const nested = root.querySelector("button[class*=\"newSession\"]");
|
|
301
|
+
if (nested !== null) return nested;
|
|
302
|
+
for (const child of root.children) if (child instanceof HTMLButtonElement && !child.matches("[data-dsh-tw-entry]")) return child;
|
|
303
|
+
const byAria = root.querySelector("button[aria-label=\"新建会话\"], button[aria-label=\"New Session\"], button[aria-label*=\"新会话\"], button[aria-label*=\"new session\" i]");
|
|
304
|
+
if (byAria !== null) return byAria;
|
|
305
|
+
return Array.from(root.querySelectorAll("button")).find((button) => !button.matches("[data-dsh-tw-entry]") && /新会话|新建会话|new session/i.test(button.textContent ?? ""));
|
|
306
|
+
}
|
|
307
|
+
/** Build the entry row (a detached button; insert once the shell is up). */
|
|
308
|
+
function createEntry(state) {
|
|
309
|
+
const entry = document.createElement("button");
|
|
310
|
+
entry.type = "button";
|
|
311
|
+
entry.dataset.dshTwEntry = "";
|
|
312
|
+
entry.className = "dsh-tw-entry";
|
|
313
|
+
entry.setAttribute("aria-label", "TiddlyWiki 知识库");
|
|
314
|
+
entry.innerHTML = `<span class="dsh-tw-entry-icon">${ICON}</span><span class="dsh-tw-entry-label">TiddlyWiki</span>`;
|
|
315
|
+
entry.addEventListener("click", () => {
|
|
316
|
+
state.toggle();
|
|
317
|
+
});
|
|
318
|
+
return entry;
|
|
319
|
+
}
|
|
320
|
+
/** Re-insert the entry before the whole family block (stable ordering). */
|
|
321
|
+
function placeEntry(root, entry) {
|
|
322
|
+
const button = newSessionButton(root);
|
|
323
|
+
if (button === void 0) return false;
|
|
324
|
+
if (entry.parentElement !== root) {
|
|
325
|
+
const row = button.closest("[class*=\"logoRow\"]");
|
|
326
|
+
const base = row !== null && row.parentElement === root ? row : button;
|
|
327
|
+
const family = Array.from(root.children).filter((el) => el instanceof HTMLElement && el.matches(FAMILY_SELECTOR));
|
|
328
|
+
const anchor = family.length > 0 ? family[0] ?? null : base.nextElementSibling ?? null;
|
|
329
|
+
root.insertBefore(entry, anchor);
|
|
330
|
+
}
|
|
331
|
+
return true;
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* Mount the sidebar entry, waiting for the shell and self-healing on later
|
|
335
|
+
* re-renders.
|
|
336
|
+
* @param state - the shared panel state the entry toggles.
|
|
337
|
+
* @returns disposer removing the entry and its observers.
|
|
338
|
+
*/
|
|
339
|
+
function mountSidebarEntry(state) {
|
|
340
|
+
const entry = createEntry(state);
|
|
341
|
+
const debug = {
|
|
342
|
+
attempts: 0,
|
|
343
|
+
found: false,
|
|
344
|
+
placed: false
|
|
345
|
+
};
|
|
346
|
+
const host = globalThis.location?.hostname;
|
|
347
|
+
if (host === "localhost" || host === "127.0.0.1") window.__twDebug = debug;
|
|
348
|
+
let root;
|
|
349
|
+
let placed = false;
|
|
350
|
+
const tryPlace = () => {
|
|
351
|
+
debug.attempts++;
|
|
352
|
+
if (root !== void 0 && !root.isConnected) {
|
|
353
|
+
rootObserver.disconnect();
|
|
354
|
+
root = void 0;
|
|
355
|
+
placed = false;
|
|
356
|
+
}
|
|
357
|
+
if (placed) {
|
|
358
|
+
if (document.body.contains(entry)) return;
|
|
359
|
+
rootObserver.disconnect();
|
|
360
|
+
root = void 0;
|
|
361
|
+
placed = false;
|
|
362
|
+
}
|
|
363
|
+
root ??= sidebarRoot();
|
|
364
|
+
if (root === void 0) return;
|
|
365
|
+
debug.found = newSessionButton(root) !== void 0;
|
|
366
|
+
placed = placeEntry(root, entry);
|
|
367
|
+
debug.placed = placed;
|
|
368
|
+
if (placed) rootObserver.observe(root, {
|
|
369
|
+
childList: true,
|
|
370
|
+
subtree: true
|
|
371
|
+
});
|
|
372
|
+
};
|
|
373
|
+
const waitObserver = new MutationObserver(() => {
|
|
374
|
+
tryPlace();
|
|
375
|
+
});
|
|
376
|
+
waitObserver.observe(document.body, {
|
|
377
|
+
childList: true,
|
|
378
|
+
subtree: true
|
|
379
|
+
});
|
|
380
|
+
const rootObserver = new MutationObserver(() => {
|
|
381
|
+
if (root === void 0 || !root.isConnected) {
|
|
382
|
+
placed = false;
|
|
383
|
+
tryPlace();
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
if (!root.contains(entry)) placed = placeEntry(root, entry);
|
|
387
|
+
});
|
|
388
|
+
const retry = setInterval(() => {
|
|
389
|
+
tryPlace();
|
|
390
|
+
}, 2e3);
|
|
391
|
+
const syncActive = () => {
|
|
392
|
+
if (state.isOpen()) entry.dataset.active = "true";
|
|
393
|
+
else delete entry.dataset.active;
|
|
394
|
+
};
|
|
395
|
+
const unsubscribe = state.subscribe(syncActive);
|
|
396
|
+
syncActive();
|
|
397
|
+
tryPlace();
|
|
398
|
+
return () => {
|
|
399
|
+
clearInterval(retry);
|
|
400
|
+
waitObserver.disconnect();
|
|
401
|
+
rootObserver.disconnect();
|
|
402
|
+
unsubscribe();
|
|
403
|
+
entry.remove();
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
//#endregion
|
|
407
|
+
//#region src/client/panel.ts
|
|
408
|
+
/**
|
|
409
|
+
* Center-column targets, most-specific shell generation first. The official
|
|
410
|
+
* layout shell (dsh-client-ui-layout) drops data-pane and uses a CSS-Module
|
|
411
|
+
* hashed `centerCol`; older shells put `data-pane="conversation"` on the same
|
|
412
|
+
* full-height grid item; DSH Desktop exposes the non-compat
|
|
413
|
+
* `.dshDesktopConversationSurface`.
|
|
414
|
+
*/
|
|
415
|
+
const COLUMN_SELECTORS = [
|
|
416
|
+
"[class*=\"centerCol\"]",
|
|
417
|
+
"[data-pane=\"conversation\"]",
|
|
418
|
+
".dshDesktopConversationSurface"
|
|
419
|
+
];
|
|
420
|
+
const ACTIVE_ATTR = "data-dsh-tw-active";
|
|
421
|
+
/** Sibling panels' activation attributes, evicted when this panel opens. */
|
|
422
|
+
const OTHER_ACTIVE_ATTRS = [
|
|
423
|
+
"data-dsh-atb-active",
|
|
424
|
+
"data-dsh-taskboard-active",
|
|
425
|
+
"data-dsh-ssh-active"
|
|
426
|
+
];
|
|
427
|
+
/** Cross-plugin activation event; detail is the activating panel name. */
|
|
428
|
+
const ACTIVATE_EVENT = "dsh-panel-activate";
|
|
429
|
+
const PANEL_NAME = "dsh-tiddlywiki";
|
|
430
|
+
/** Overlay z-index: above the shell content, below the note widget (950). */
|
|
431
|
+
const PANEL_Z_INDEX = 40;
|
|
432
|
+
/** Safety re-measure cadence for shell layout changes CSS can't see. */
|
|
433
|
+
const SYNC_INTERVAL_MS = 2e3;
|
|
434
|
+
const STATUS_ENDPOINT$1 = "/dsh-tiddlywiki/status";
|
|
435
|
+
const RESTART_ENDPOINT$1 = "/dsh-tiddlywiki/restart";
|
|
436
|
+
function conversationColumn() {
|
|
437
|
+
for (const selector of COLUMN_SELECTORS) {
|
|
438
|
+
const el = document.querySelector(selector);
|
|
439
|
+
if (el !== null) return el;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
async function fetchStatus() {
|
|
443
|
+
try {
|
|
444
|
+
const res = await fetch(STATUS_ENDPOINT$1, { signal: AbortSignal.timeout(8e3) });
|
|
445
|
+
if (!res.ok) return null;
|
|
446
|
+
return await res.json();
|
|
447
|
+
} catch {
|
|
448
|
+
return null;
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
async function requestRestart() {
|
|
452
|
+
try {
|
|
453
|
+
return (await fetch(RESTART_ENDPOINT$1, {
|
|
454
|
+
method: "POST",
|
|
455
|
+
signal: AbortSignal.timeout(8e3)
|
|
456
|
+
})).ok;
|
|
457
|
+
} catch {
|
|
458
|
+
return false;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
function mountPanel(state) {
|
|
462
|
+
let container;
|
|
463
|
+
let columnEl;
|
|
464
|
+
let iframe;
|
|
465
|
+
let frameArea;
|
|
466
|
+
let errorArea;
|
|
467
|
+
let chip;
|
|
468
|
+
let refreshTimer;
|
|
469
|
+
let refreshAttempts = 0;
|
|
470
|
+
const build = () => {
|
|
471
|
+
const view = document.createElement("div");
|
|
472
|
+
view.dataset.dshTwView = "";
|
|
473
|
+
view.className = "dsh-tw-view";
|
|
474
|
+
const bar = document.createElement("div");
|
|
475
|
+
bar.className = "dsh-tw-panel-bar";
|
|
476
|
+
const title = document.createElement("span");
|
|
477
|
+
title.className = "dsh-tw-panel-title";
|
|
478
|
+
title.textContent = "TiddlyWiki 知识库";
|
|
479
|
+
chip = document.createElement("span");
|
|
480
|
+
chip.className = "dsh-tw-status-chip";
|
|
481
|
+
chip.dataset.state = "unknown";
|
|
482
|
+
chip.textContent = "—";
|
|
483
|
+
const reload = document.createElement("button");
|
|
484
|
+
reload.type = "button";
|
|
485
|
+
reload.textContent = "重载";
|
|
486
|
+
reload.addEventListener("click", () => {
|
|
487
|
+
if (iframe !== void 0 && !iframe.hidden) iframe.src = iframe.src;
|
|
488
|
+
});
|
|
489
|
+
const refresh = document.createElement("button");
|
|
490
|
+
refresh.type = "button";
|
|
491
|
+
refresh.textContent = "状态";
|
|
492
|
+
refresh.addEventListener("click", () => {
|
|
493
|
+
doRefresh();
|
|
494
|
+
});
|
|
495
|
+
bar.append(title, chip, refresh, reload);
|
|
496
|
+
frameArea = document.createElement("div");
|
|
497
|
+
frameArea.className = "dsh-tw-panel-frame-wrap";
|
|
498
|
+
frameArea.style.cssText = "flex:1;min-height:0;display:flex;flex-direction:column";
|
|
499
|
+
iframe = document.createElement("iframe");
|
|
500
|
+
iframe.className = "dsh-tw-panel-frame";
|
|
501
|
+
iframe.title = "TiddlyWiki";
|
|
502
|
+
iframe.hidden = true;
|
|
503
|
+
frameArea.append(iframe);
|
|
504
|
+
errorArea = document.createElement("div");
|
|
505
|
+
errorArea.className = "dsh-tw-panel-error";
|
|
506
|
+
errorArea.hidden = true;
|
|
507
|
+
view.append(bar, frameArea, errorArea);
|
|
508
|
+
return view;
|
|
509
|
+
};
|
|
510
|
+
const setChip = (stateName, text) => {
|
|
511
|
+
if (chip === void 0) return;
|
|
512
|
+
chip.dataset.state = stateName;
|
|
513
|
+
chip.textContent = text;
|
|
514
|
+
};
|
|
515
|
+
/** Pin the overlay to the center column's current viewport rect. */
|
|
516
|
+
const syncRect = () => {
|
|
517
|
+
if (container === void 0 || columnEl === void 0) return;
|
|
518
|
+
const rect = columnEl.getBoundingClientRect();
|
|
519
|
+
if (rect.width === 0 && rect.height === 0) return;
|
|
520
|
+
const left = `${rect.left}px`;
|
|
521
|
+
const top = `${rect.top}px`;
|
|
522
|
+
const width = `${rect.width}px`;
|
|
523
|
+
const height = `${rect.height}px`;
|
|
524
|
+
if (container.style.left !== left) container.style.left = left;
|
|
525
|
+
if (container.style.top !== top) container.style.top = top;
|
|
526
|
+
if (container.style.width !== width) container.style.width = width;
|
|
527
|
+
if (container.style.height !== height) container.style.height = height;
|
|
528
|
+
};
|
|
529
|
+
const ensure = () => {
|
|
530
|
+
if (container !== void 0) return;
|
|
531
|
+
columnEl = conversationColumn();
|
|
532
|
+
if (columnEl === void 0) return;
|
|
533
|
+
container = build();
|
|
534
|
+
container.style.position = "fixed";
|
|
535
|
+
container.style.zIndex = String(PANEL_Z_INDEX);
|
|
536
|
+
document.body.append(container);
|
|
537
|
+
syncRect();
|
|
538
|
+
};
|
|
539
|
+
const showError = (message) => {
|
|
540
|
+
if (iframe === void 0 || errorArea === void 0 || frameArea === void 0) return;
|
|
541
|
+
iframe.hidden = true;
|
|
542
|
+
errorArea.hidden = false;
|
|
543
|
+
errorArea.textContent = "";
|
|
544
|
+
const p = document.createElement("div");
|
|
545
|
+
p.textContent = "TiddlyWiki 服务不可用";
|
|
546
|
+
const code = document.createElement("code");
|
|
547
|
+
code.textContent = message;
|
|
548
|
+
const retry = document.createElement("button");
|
|
549
|
+
retry.type = "button";
|
|
550
|
+
retry.textContent = "重试";
|
|
551
|
+
retry.addEventListener("click", () => {
|
|
552
|
+
retry.disabled = true;
|
|
553
|
+
retry.textContent = "重启中…";
|
|
554
|
+
requestRestart().finally(() => {
|
|
555
|
+
doRefresh();
|
|
556
|
+
});
|
|
557
|
+
});
|
|
558
|
+
errorArea.append(p, code, retry);
|
|
559
|
+
};
|
|
560
|
+
const showStarting = () => {
|
|
561
|
+
if (iframe === void 0 || errorArea === void 0 || frameArea === void 0) return;
|
|
562
|
+
iframe.hidden = true;
|
|
563
|
+
errorArea.hidden = false;
|
|
564
|
+
errorArea.textContent = "";
|
|
565
|
+
const p = document.createElement("div");
|
|
566
|
+
p.textContent = "TiddlyWiki 服务正在启动…";
|
|
567
|
+
errorArea.append(p);
|
|
568
|
+
};
|
|
569
|
+
const showFrame = (url) => {
|
|
570
|
+
if (iframe === void 0 || errorArea === void 0) return;
|
|
571
|
+
errorArea.hidden = true;
|
|
572
|
+
iframe.hidden = false;
|
|
573
|
+
if (iframe.dataset.loaded !== url) {
|
|
574
|
+
iframe.dataset.loaded = url;
|
|
575
|
+
iframe.src = url;
|
|
576
|
+
}
|
|
577
|
+
};
|
|
578
|
+
const doRefresh = async () => {
|
|
579
|
+
if (refreshTimer !== void 0) {
|
|
580
|
+
window.clearTimeout(refreshTimer);
|
|
581
|
+
refreshTimer = void 0;
|
|
582
|
+
}
|
|
583
|
+
const payload = await fetchStatus();
|
|
584
|
+
if (payload === null) {
|
|
585
|
+
setChip("failed", "状态不可达");
|
|
586
|
+
showError("无法访问 /dsh-tiddlywiki/status");
|
|
587
|
+
return;
|
|
588
|
+
}
|
|
589
|
+
if (payload.status === "running" && typeof payload.url === "string") {
|
|
590
|
+
setChip("running", "在线");
|
|
591
|
+
refreshAttempts = 0;
|
|
592
|
+
showFrame(payload.url);
|
|
593
|
+
return;
|
|
594
|
+
}
|
|
595
|
+
if (payload.status === "starting") {
|
|
596
|
+
setChip("starting", "启动中");
|
|
597
|
+
showStarting();
|
|
598
|
+
if (refreshAttempts < 30) {
|
|
599
|
+
refreshAttempts++;
|
|
600
|
+
refreshTimer = window.setTimeout(() => {
|
|
601
|
+
doRefresh();
|
|
602
|
+
}, 1500);
|
|
603
|
+
}
|
|
604
|
+
return;
|
|
605
|
+
}
|
|
606
|
+
setChip("failed", "离线");
|
|
607
|
+
refreshAttempts = 0;
|
|
608
|
+
showError(payload.error ?? `服务状态:${payload.status}`);
|
|
609
|
+
};
|
|
610
|
+
const applyActive = () => {
|
|
611
|
+
if (state.isOpen()) {
|
|
612
|
+
for (const attr of OTHER_ACTIVE_ATTRS) document.documentElement.removeAttribute(attr);
|
|
613
|
+
document.documentElement.setAttribute(ACTIVE_ATTR, "");
|
|
614
|
+
document.dispatchEvent(new CustomEvent(ACTIVATE_EVENT, { detail: PANEL_NAME }));
|
|
615
|
+
doRefresh();
|
|
616
|
+
} else {
|
|
617
|
+
document.documentElement.removeAttribute(ACTIVE_ATTR);
|
|
618
|
+
if (refreshTimer !== void 0) {
|
|
619
|
+
window.clearTimeout(refreshTimer);
|
|
620
|
+
refreshTimer = void 0;
|
|
621
|
+
}
|
|
622
|
+
refreshAttempts = 0;
|
|
623
|
+
}
|
|
624
|
+
};
|
|
625
|
+
const onOtherActivate = (event) => {
|
|
626
|
+
if (event.detail !== PANEL_NAME && state.isOpen()) state.closePanel();
|
|
627
|
+
};
|
|
628
|
+
const onClickSidebarRow = (event) => {
|
|
629
|
+
if (!state.isOpen()) return;
|
|
630
|
+
const target = event.target;
|
|
631
|
+
if (target === null) return;
|
|
632
|
+
if (target.closest("[data-dsh-tw-entry]") !== null) return;
|
|
633
|
+
if (target.closest("[class*=\"sessionRow\"], [class*=\"projectRow\"], [class*=\"searchResultRow\"], [class*=\"searchResultWorkspace\"], [class*=\"newSession\"]") !== null) state.closePanel();
|
|
634
|
+
};
|
|
635
|
+
const waitObserver = new MutationObserver(() => {
|
|
636
|
+
ensure();
|
|
637
|
+
});
|
|
638
|
+
waitObserver.observe(document.body, {
|
|
639
|
+
childList: true,
|
|
640
|
+
subtree: true
|
|
641
|
+
});
|
|
642
|
+
const resizeObserver = new ResizeObserver(() => syncRect());
|
|
643
|
+
resizeObserver.observe(document.body);
|
|
644
|
+
const syncInterval = window.setInterval(syncRect, SYNC_INTERVAL_MS);
|
|
645
|
+
const onWindowResize = () => syncRect();
|
|
646
|
+
window.addEventListener("resize", onWindowResize);
|
|
647
|
+
const onAnyScroll = () => syncRect();
|
|
648
|
+
window.addEventListener("scroll", onAnyScroll, true);
|
|
649
|
+
document.addEventListener("click", onClickSidebarRow, true);
|
|
650
|
+
document.addEventListener(ACTIVATE_EVENT, onOtherActivate);
|
|
651
|
+
const unsubscribe = state.subscribe(applyActive);
|
|
652
|
+
ensure();
|
|
653
|
+
applyActive();
|
|
654
|
+
return () => {
|
|
655
|
+
if (refreshTimer !== void 0) window.clearTimeout(refreshTimer);
|
|
656
|
+
window.clearInterval(syncInterval);
|
|
657
|
+
window.removeEventListener("resize", onWindowResize);
|
|
658
|
+
window.removeEventListener("scroll", onAnyScroll, true);
|
|
659
|
+
resizeObserver.disconnect();
|
|
660
|
+
document.removeEventListener("click", onClickSidebarRow, true);
|
|
661
|
+
document.removeEventListener(ACTIVATE_EVENT, onOtherActivate);
|
|
662
|
+
waitObserver.disconnect();
|
|
663
|
+
unsubscribe();
|
|
664
|
+
document.documentElement.removeAttribute(ACTIVE_ATTR);
|
|
665
|
+
container?.remove();
|
|
666
|
+
};
|
|
667
|
+
}
|
|
668
|
+
//#endregion
|
|
669
|
+
//#region src/client/toast.ts
|
|
670
|
+
/**
|
|
671
|
+
* One-shot transient toast, shared by the quick-note widget and the settings
|
|
672
|
+
* page. Reuses a single body-level element (`.dsh-tw-toast`) with a show
|
|
673
|
+
* class; the element is created lazily and styled by styles.ts.
|
|
674
|
+
*
|
|
675
|
+
* @module dsh-tiddlywiki/client/toast
|
|
676
|
+
*/
|
|
677
|
+
function toast(message) {
|
|
678
|
+
let el = document.querySelector(".dsh-tw-toast");
|
|
679
|
+
if (el === null) {
|
|
680
|
+
el = document.createElement("div");
|
|
681
|
+
el.className = "dsh-tw-toast";
|
|
682
|
+
document.body.append(el);
|
|
683
|
+
}
|
|
684
|
+
el.textContent = message;
|
|
685
|
+
el.classList.add("dsh-tw-toast-show");
|
|
686
|
+
clearTimeout(el.__twToastTimer);
|
|
687
|
+
el.__twToastTimer = window.setTimeout(() => {
|
|
688
|
+
el?.classList.remove("dsh-tw-toast-show");
|
|
689
|
+
}, 2500);
|
|
690
|
+
}
|
|
691
|
+
//#endregion
|
|
692
|
+
//#region src/client/editor-popup.ts
|
|
693
|
+
/**
|
|
694
|
+
* Floating popup iframe that loads TiddlyWiki's NATIVE editor for a draft
|
|
695
|
+
* (quick-note "✏️ 在 TW 中编辑"). A small draggable + resizable overlay with its
|
|
696
|
+
* own iframe pointed at `twUrl#<draftTitle>`: the fragment-only navigation
|
|
697
|
+
* triggers TW's hashchange, which opens the draft in the story, and because the
|
|
698
|
+
* draft tiddler carries `draft.of` the story renders the native EditTemplate.
|
|
699
|
+
*
|
|
700
|
+
* Independent of the center panel — a separate floating window so the user can
|
|
701
|
+
* edit a note without leaving the chat context.
|
|
702
|
+
*
|
|
703
|
+
* @module dsh-tiddlywiki/client/editor-popup
|
|
704
|
+
*/
|
|
705
|
+
let root;
|
|
706
|
+
let frame;
|
|
707
|
+
let titleEl;
|
|
708
|
+
/** Open (create on first use) the popup and load `url` (twUrl#draftTitle). */
|
|
709
|
+
function openEditorPopup(url, label) {
|
|
710
|
+
ensurePopup();
|
|
711
|
+
if (root === void 0 || frame === void 0) return;
|
|
712
|
+
if (titleEl !== void 0) titleEl.textContent = `TiddlyWiki 编辑器 · ${label}`;
|
|
713
|
+
root.style.display = "";
|
|
714
|
+
frame.src = url;
|
|
715
|
+
}
|
|
716
|
+
/** Remove the popup DOM entirely (plugin dispose). */
|
|
717
|
+
function disposeEditorPopup() {
|
|
718
|
+
root?.remove();
|
|
719
|
+
root = void 0;
|
|
720
|
+
frame = void 0;
|
|
721
|
+
titleEl = void 0;
|
|
722
|
+
}
|
|
723
|
+
function ensurePopup() {
|
|
724
|
+
if (root !== void 0 && frame !== void 0) return;
|
|
725
|
+
root = document.createElement("div");
|
|
726
|
+
root.className = "dsh-tw-editor-popup";
|
|
727
|
+
root.style.display = "none";
|
|
728
|
+
const bar = document.createElement("div");
|
|
729
|
+
bar.className = "dsh-tw-editor-bar";
|
|
730
|
+
titleEl = document.createElement("span");
|
|
731
|
+
titleEl.className = "dsh-tw-editor-title";
|
|
732
|
+
titleEl.textContent = "TiddlyWiki 编辑器";
|
|
733
|
+
const close = document.createElement("button");
|
|
734
|
+
close.type = "button";
|
|
735
|
+
close.className = "dsh-tw-editor-close";
|
|
736
|
+
close.textContent = "✕";
|
|
737
|
+
close.title = "关闭";
|
|
738
|
+
bar.append(titleEl, close);
|
|
739
|
+
frame = document.createElement("iframe");
|
|
740
|
+
frame.className = "dsh-tw-editor-frame";
|
|
741
|
+
frame.setAttribute("sandbox", "allow-scripts allow-same-origin allow-forms allow-popups");
|
|
742
|
+
frame.title = "TiddlyWiki 编辑器";
|
|
743
|
+
const resize = document.createElement("div");
|
|
744
|
+
resize.className = "dsh-tw-editor-resize";
|
|
745
|
+
resize.title = "拖拽调整大小";
|
|
746
|
+
root.append(bar, frame, resize);
|
|
747
|
+
document.body.append(root);
|
|
748
|
+
close.addEventListener("click", () => {
|
|
749
|
+
if (root !== void 0) root.style.display = "none";
|
|
750
|
+
});
|
|
751
|
+
bar.addEventListener("mousedown", (event) => {
|
|
752
|
+
if (event.button !== 0 || root === void 0) return;
|
|
753
|
+
event.preventDefault();
|
|
754
|
+
const rect = root.getBoundingClientRect();
|
|
755
|
+
const startX = event.clientX;
|
|
756
|
+
const startY = event.clientY;
|
|
757
|
+
const baseLeft = rect.left;
|
|
758
|
+
const baseTop = rect.top;
|
|
759
|
+
const onMove = (ev) => {
|
|
760
|
+
if (root === void 0) return;
|
|
761
|
+
root.style.left = `${baseLeft + ev.clientX - startX}px`;
|
|
762
|
+
root.style.top = `${baseTop + ev.clientY - startY}px`;
|
|
763
|
+
root.style.margin = "0";
|
|
764
|
+
root.style.right = "auto";
|
|
765
|
+
root.style.bottom = "auto";
|
|
766
|
+
};
|
|
767
|
+
const onUp = () => {
|
|
768
|
+
window.removeEventListener("mousemove", onMove);
|
|
769
|
+
window.removeEventListener("mouseup", onUp);
|
|
770
|
+
};
|
|
771
|
+
window.addEventListener("mousemove", onMove);
|
|
772
|
+
window.addEventListener("mouseup", onUp);
|
|
773
|
+
});
|
|
774
|
+
resize.addEventListener("mousedown", (event) => {
|
|
775
|
+
if (event.button !== 0 || root === void 0) return;
|
|
776
|
+
event.preventDefault();
|
|
777
|
+
event.stopPropagation();
|
|
778
|
+
const rect = root.getBoundingClientRect();
|
|
779
|
+
const startX = event.clientX;
|
|
780
|
+
const startY = event.clientY;
|
|
781
|
+
const baseW = rect.width;
|
|
782
|
+
const baseH = rect.height;
|
|
783
|
+
const onMove = (ev) => {
|
|
784
|
+
if (root === void 0) return;
|
|
785
|
+
root.style.width = `${Math.max(360, baseW + ev.clientX - startX)}px`;
|
|
786
|
+
root.style.height = `${Math.max(260, baseH + ev.clientY - startY)}px`;
|
|
787
|
+
root.style.right = "auto";
|
|
788
|
+
root.style.bottom = "auto";
|
|
789
|
+
};
|
|
790
|
+
const onUp = () => {
|
|
791
|
+
window.removeEventListener("mousemove", onMove);
|
|
792
|
+
window.removeEventListener("mouseup", onUp);
|
|
793
|
+
};
|
|
794
|
+
window.addEventListener("mousemove", onMove);
|
|
795
|
+
window.addEventListener("mouseup", onUp);
|
|
796
|
+
});
|
|
797
|
+
}
|
|
798
|
+
//#endregion
|
|
799
|
+
//#region src/client/note-widget.ts
|
|
800
|
+
/**
|
|
801
|
+
* Floating quick-note widget (design doc §12, D6/D7) — bottom-right, fixed,
|
|
802
|
+
* collapsible, independent of any shell DOM. Lets the human jot drafts /
|
|
803
|
+
* scratch notes while waiting for the AI or drafting the next prompt.
|
|
804
|
+
*
|
|
805
|
+
* Save posts to /dsh-tiddlywiki/note → an independent tiddler (title & tag
|
|
806
|
+
* editable; defaults: timestamp title + config tag, usually "inbox").
|
|
807
|
+
*
|
|
808
|
+
* @module dsh-tiddlywiki/client/note-widget
|
|
809
|
+
*/
|
|
810
|
+
const NOTE_ENDPOINT = "/dsh-tiddlywiki/note";
|
|
811
|
+
const EDIT_ENDPOINT = "/dsh-tiddlywiki/edit";
|
|
812
|
+
const STATUS_ENDPOINT = "/dsh-tiddlywiki/status";
|
|
813
|
+
function pad(n) {
|
|
814
|
+
return n < 10 ? `0${n}` : String(n);
|
|
815
|
+
}
|
|
816
|
+
/** Default note title: `YYYY-MM-DD HH:mm`. */
|
|
817
|
+
function timestampTitle(date = /* @__PURE__ */ new Date()) {
|
|
818
|
+
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}`;
|
|
819
|
+
}
|
|
820
|
+
async function fetchDefaultTag() {
|
|
821
|
+
try {
|
|
822
|
+
const res = await fetch(STATUS_ENDPOINT, { signal: AbortSignal.timeout(5e3) });
|
|
823
|
+
if (!res.ok) return "inbox";
|
|
824
|
+
return (await res.json()).note?.tag ?? "inbox";
|
|
825
|
+
} catch {
|
|
826
|
+
return "inbox";
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
function mountNoteWidget() {
|
|
830
|
+
const root = document.createElement("div");
|
|
831
|
+
root.className = "dsh-tw-note";
|
|
832
|
+
const card = document.createElement("div");
|
|
833
|
+
card.className = "dsh-tw-note-card";
|
|
834
|
+
card.hidden = true;
|
|
835
|
+
const head = document.createElement("div");
|
|
836
|
+
head.className = "dsh-tw-note-head";
|
|
837
|
+
const label = document.createElement("span");
|
|
838
|
+
label.className = "dsh-tw-note-label";
|
|
839
|
+
label.textContent = "📝 快速笔记";
|
|
840
|
+
const closeBtn = document.createElement("button");
|
|
841
|
+
closeBtn.type = "button";
|
|
842
|
+
closeBtn.className = "dsh-tw-note-close";
|
|
843
|
+
closeBtn.title = "收起";
|
|
844
|
+
closeBtn.textContent = "✕";
|
|
845
|
+
head.append(label, closeBtn);
|
|
846
|
+
const fields = document.createElement("div");
|
|
847
|
+
fields.className = "dsh-tw-note-fields";
|
|
848
|
+
const titleInput = document.createElement("input");
|
|
849
|
+
titleInput.className = "dsh-tw-note-title";
|
|
850
|
+
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);
|
|
855
|
+
const textarea = document.createElement("textarea");
|
|
856
|
+
textarea.className = "dsh-tw-note-text";
|
|
857
|
+
textarea.placeholder = "写点东西…(Ctrl+Enter 保存)";
|
|
858
|
+
const foot = document.createElement("div");
|
|
859
|
+
foot.className = "dsh-tw-note-foot";
|
|
860
|
+
const hint = document.createElement("span");
|
|
861
|
+
hint.className = "dsh-tw-note-hint";
|
|
862
|
+
hint.textContent = "Ctrl+Enter";
|
|
863
|
+
const edit = document.createElement("button");
|
|
864
|
+
edit.type = "button";
|
|
865
|
+
edit.className = "dsh-tw-note-edit";
|
|
866
|
+
edit.title = "保存并在 TiddlyWiki 原生编辑器中打开";
|
|
867
|
+
edit.textContent = "✏️ 在 TW 中编辑";
|
|
868
|
+
const save = document.createElement("button");
|
|
869
|
+
save.type = "button";
|
|
870
|
+
save.className = "dsh-tw-note-save";
|
|
871
|
+
save.textContent = "保存";
|
|
872
|
+
foot.append(hint, edit, save);
|
|
873
|
+
card.append(head, fields, textarea, foot);
|
|
874
|
+
const toggle = document.createElement("button");
|
|
875
|
+
toggle.type = "button";
|
|
876
|
+
toggle.className = "dsh-tw-note-toggle";
|
|
877
|
+
toggle.textContent = "📝 快速笔记";
|
|
878
|
+
root.append(card, toggle);
|
|
879
|
+
document.body.append(root);
|
|
880
|
+
let defaultTag = "inbox";
|
|
881
|
+
let opened = false;
|
|
882
|
+
const resetTitle = () => {
|
|
883
|
+
titleInput.value = timestampTitle();
|
|
884
|
+
};
|
|
885
|
+
const open = async () => {
|
|
886
|
+
card.hidden = false;
|
|
887
|
+
opened = true;
|
|
888
|
+
resetTitle();
|
|
889
|
+
if (tagInput.value.length === 0) tagInput.value = defaultTag;
|
|
890
|
+
textarea.focus();
|
|
891
|
+
defaultTag = await fetchDefaultTag();
|
|
892
|
+
if (tagInput.value.length === 0) tagInput.value = defaultTag;
|
|
893
|
+
};
|
|
894
|
+
const close = () => {
|
|
895
|
+
card.hidden = true;
|
|
896
|
+
opened = false;
|
|
897
|
+
};
|
|
898
|
+
toggle.addEventListener("click", () => {
|
|
899
|
+
opened ? close() : open();
|
|
900
|
+
});
|
|
901
|
+
closeBtn.addEventListener("click", close);
|
|
902
|
+
const doSave = async () => {
|
|
903
|
+
const text = textarea.value.trim();
|
|
904
|
+
if (text.length === 0) {
|
|
905
|
+
toast("内容为空,未保存");
|
|
906
|
+
return;
|
|
907
|
+
}
|
|
908
|
+
save.disabled = true;
|
|
909
|
+
save.textContent = "保存中…";
|
|
910
|
+
try {
|
|
911
|
+
const res = await fetch(NOTE_ENDPOINT, {
|
|
912
|
+
method: "POST",
|
|
913
|
+
headers: { "content-type": "application/json" },
|
|
914
|
+
body: JSON.stringify({
|
|
915
|
+
title: titleInput.value.trim(),
|
|
916
|
+
tag: tagInput.value.trim(),
|
|
917
|
+
text
|
|
918
|
+
}),
|
|
919
|
+
signal: AbortSignal.timeout(1e4)
|
|
920
|
+
});
|
|
921
|
+
const payload = await res.json().catch(() => null);
|
|
922
|
+
if (!res.ok || payload?.ok !== true) {
|
|
923
|
+
toast(`保存失败:${payload?.error ?? `HTTP ${res.status}`}`);
|
|
924
|
+
return;
|
|
925
|
+
}
|
|
926
|
+
textarea.value = "";
|
|
927
|
+
resetTitle();
|
|
928
|
+
toast(`已保存「${payload.title ?? titleInput.value}」`);
|
|
929
|
+
close();
|
|
930
|
+
} catch (err) {
|
|
931
|
+
toast(`保存失败:${err instanceof Error ? err.message : String(err)}`);
|
|
932
|
+
} finally {
|
|
933
|
+
save.disabled = false;
|
|
934
|
+
save.textContent = "保存";
|
|
935
|
+
}
|
|
936
|
+
};
|
|
937
|
+
save.addEventListener("click", () => {
|
|
938
|
+
doSave();
|
|
939
|
+
});
|
|
940
|
+
textarea.addEventListener("keydown", (event) => {
|
|
941
|
+
if ((event.ctrlKey || event.metaKey) && event.key === "Enter") {
|
|
942
|
+
event.preventDefault();
|
|
943
|
+
doSave();
|
|
944
|
+
}
|
|
945
|
+
});
|
|
946
|
+
/** Save (if non-empty) and open the tiddler in TW's native editor. */
|
|
947
|
+
const doEdit = async () => {
|
|
948
|
+
const title = titleInput.value.trim().length > 0 ? titleInput.value.trim() : timestampTitle();
|
|
949
|
+
const text = textarea.value;
|
|
950
|
+
const tag = tagInput.value.trim();
|
|
951
|
+
edit.disabled = true;
|
|
952
|
+
edit.textContent = "打开中…";
|
|
953
|
+
try {
|
|
954
|
+
const res = await fetch(EDIT_ENDPOINT, {
|
|
955
|
+
method: "POST",
|
|
956
|
+
headers: { "content-type": "application/json" },
|
|
957
|
+
body: JSON.stringify({
|
|
958
|
+
title,
|
|
959
|
+
tag,
|
|
960
|
+
text
|
|
961
|
+
}),
|
|
962
|
+
signal: AbortSignal.timeout(1e4)
|
|
963
|
+
});
|
|
964
|
+
const payload = await res.json().catch(() => null);
|
|
965
|
+
if (!res.ok || payload?.ok !== true) {
|
|
966
|
+
toast(`打开失败:${payload?.error ?? `HTTP ${res.status}`}`);
|
|
967
|
+
return;
|
|
968
|
+
}
|
|
969
|
+
if (typeof payload.twUrl !== "string" || typeof payload.draftTitle !== "string") {
|
|
970
|
+
toast("打开失败:服务未返回编辑器地址");
|
|
971
|
+
return;
|
|
972
|
+
}
|
|
973
|
+
openEditorPopup(`${payload.twUrl}#${encodeURIComponent(payload.draftTitle)}`, payload.title ?? title);
|
|
974
|
+
toast(`已在弹出窗口打开「${payload.title ?? title}」编辑器`);
|
|
975
|
+
} catch (err) {
|
|
976
|
+
toast(`打开失败:${err instanceof Error ? err.message : String(err)}`);
|
|
977
|
+
} finally {
|
|
978
|
+
edit.disabled = false;
|
|
979
|
+
edit.textContent = "✏️ 在 TW 中编辑";
|
|
980
|
+
}
|
|
981
|
+
};
|
|
982
|
+
edit.addEventListener("click", () => {
|
|
983
|
+
doEdit();
|
|
984
|
+
});
|
|
985
|
+
return () => {
|
|
986
|
+
root.remove();
|
|
987
|
+
document.querySelector(".dsh-tw-toast")?.remove();
|
|
988
|
+
};
|
|
989
|
+
}
|
|
990
|
+
//#endregion
|
|
991
|
+
//#region src/client/settings-page.ts
|
|
992
|
+
/**
|
|
993
|
+
* Settings-page half (design doc §13, config panel): a pure-DOM page mounted
|
|
994
|
+
* inside a `settings.section` React wrapper. Everything talks to the host
|
|
995
|
+
* admin routes — same-origin JSON, no client services beyond `slots`:
|
|
996
|
+
*
|
|
997
|
+
* GET /dsh-tiddlywiki/admin/state current info + catalog + config
|
|
998
|
+
* POST /dsh-tiddlywiki/admin/info { plugins?, themes? } → restart TW
|
|
999
|
+
* POST /dsh-tiddlywiki/admin/config { ...patch } → persist
|
|
1000
|
+
* POST /dsh-tiddlywiki/admin/restart restart the TW child
|
|
1001
|
+
*
|
|
1002
|
+
* Sections:
|
|
1003
|
+
* 1. 状态/重启 TW 运行状态 + git 概览 + 重启按钮
|
|
1004
|
+
* 2. 常规配置 note.tag / git.* / uiLanguage(改了什么保存什么)
|
|
1005
|
+
* 3. 插件管理 自带官方插件勾选(可搜索)→ 应用并重启 TW
|
|
1006
|
+
* 4. 主题管理 自带主题单选 → 应用并重启 TW
|
|
1007
|
+
*
|
|
1008
|
+
* @module dsh-tiddlywiki/client/settings-page
|
|
1009
|
+
*/
|
|
1010
|
+
const STATE_ENDPOINT = "/dsh-tiddlywiki/admin/state";
|
|
1011
|
+
const INFO_ENDPOINT = "/dsh-tiddlywiki/admin/info";
|
|
1012
|
+
const CONFIG_ENDPOINT = "/dsh-tiddlywiki/admin/config";
|
|
1013
|
+
const RESTART_ENDPOINT = "/dsh-tiddlywiki/admin/restart";
|
|
1014
|
+
function make(tag, className, text) {
|
|
1015
|
+
const node = document.createElement(tag);
|
|
1016
|
+
if (className !== void 0) node.className = className;
|
|
1017
|
+
if (text !== void 0) node.textContent = text;
|
|
1018
|
+
return node;
|
|
1019
|
+
}
|
|
1020
|
+
async function fetchJson(url, init) {
|
|
1021
|
+
const res = await fetch(url, {
|
|
1022
|
+
...init,
|
|
1023
|
+
signal: AbortSignal.timeout(15e3)
|
|
1024
|
+
});
|
|
1025
|
+
const data = await res.json().catch(() => ({}));
|
|
1026
|
+
if (!res.ok) {
|
|
1027
|
+
const err = data.error ?? `HTTP ${res.status}`;
|
|
1028
|
+
throw new Error(err);
|
|
1029
|
+
}
|
|
1030
|
+
return data;
|
|
1031
|
+
}
|
|
1032
|
+
function mountSettingsPage(container) {
|
|
1033
|
+
let disposed = false;
|
|
1034
|
+
container.classList.add("dsh-tw-settings");
|
|
1035
|
+
const statusRow = make("div", "dsh-tw-settings-row dsh-tw-settings-status");
|
|
1036
|
+
const body = make("div", "dsh-tw-settings-body");
|
|
1037
|
+
container.append(statusRow, body);
|
|
1038
|
+
const disposers = [];
|
|
1039
|
+
const refresh = async () => {
|
|
1040
|
+
try {
|
|
1041
|
+
const state = await fetchJson(STATE_ENDPOINT);
|
|
1042
|
+
if (disposed) return;
|
|
1043
|
+
renderStatus(statusRow, state, refresh);
|
|
1044
|
+
renderMain(body, state, refresh);
|
|
1045
|
+
} catch (err) {
|
|
1046
|
+
if (disposed) return;
|
|
1047
|
+
body.replaceChildren();
|
|
1048
|
+
statusRow.replaceChildren();
|
|
1049
|
+
const msg = make("div", "dsh-tw-settings-error", `加载配置失败:${err instanceof Error ? err.message : String(err)}`);
|
|
1050
|
+
const retry = make("button", "dsh-tw-settings-btn", "重试");
|
|
1051
|
+
retry.type = "button";
|
|
1052
|
+
retry.addEventListener("click", () => {
|
|
1053
|
+
refresh();
|
|
1054
|
+
});
|
|
1055
|
+
body.append(msg, retry);
|
|
1056
|
+
}
|
|
1057
|
+
};
|
|
1058
|
+
refresh();
|
|
1059
|
+
disposers.push(() => {
|
|
1060
|
+
disposed = true;
|
|
1061
|
+
container.replaceChildren();
|
|
1062
|
+
container.classList.remove("dsh-tw-settings");
|
|
1063
|
+
});
|
|
1064
|
+
return () => {
|
|
1065
|
+
for (const dispose of disposers.splice(0)) dispose();
|
|
1066
|
+
};
|
|
1067
|
+
}
|
|
1068
|
+
function renderStatus(row, state, refresh) {
|
|
1069
|
+
row.replaceChildren();
|
|
1070
|
+
const server = state.server ?? {};
|
|
1071
|
+
const status = server.status ?? "unknown";
|
|
1072
|
+
const chip = make("span", `dsh-tw-settings-chip`, status);
|
|
1073
|
+
chip.dataset.state = status;
|
|
1074
|
+
const label = make("span", "dsh-tw-settings-muted", [
|
|
1075
|
+
server.url !== void 0 ? `TW ${server.url}` : "",
|
|
1076
|
+
state.git?.branch !== void 0 ? `git ${state.git.branch}` : "",
|
|
1077
|
+
state.git?.lastCommit !== void 0 ? state.git.lastCommit : "",
|
|
1078
|
+
state.git?.dirty === true ? "有未提交改动" : ""
|
|
1079
|
+
].filter(Boolean).join(" · "));
|
|
1080
|
+
const restart = make("button", "dsh-tw-settings-btn", "重启 TW");
|
|
1081
|
+
restart.type = "button";
|
|
1082
|
+
restart.addEventListener("click", () => {
|
|
1083
|
+
restart.disabled = true;
|
|
1084
|
+
restart.textContent = "重启中…";
|
|
1085
|
+
(async () => {
|
|
1086
|
+
try {
|
|
1087
|
+
await fetchJson(RESTART_ENDPOINT, { method: "POST" });
|
|
1088
|
+
toast("TW 已重启");
|
|
1089
|
+
} catch (err) {
|
|
1090
|
+
toast(`重启失败:${err instanceof Error ? err.message : String(err)}`);
|
|
1091
|
+
} finally {
|
|
1092
|
+
restart.disabled = false;
|
|
1093
|
+
restart.textContent = "重启 TW";
|
|
1094
|
+
refresh();
|
|
1095
|
+
}
|
|
1096
|
+
})();
|
|
1097
|
+
});
|
|
1098
|
+
row.append(chip, label, restart);
|
|
1099
|
+
}
|
|
1100
|
+
/** Config section: fields bound to effective config, changed-only save. */
|
|
1101
|
+
function renderConfigSection(body, config, refresh) {
|
|
1102
|
+
const section = make("section", "dsh-tw-settings-section");
|
|
1103
|
+
section.append(make("h3", "dsh-tw-settings-h", "常规配置"));
|
|
1104
|
+
const note = config.note ?? {};
|
|
1105
|
+
const git = config.git ?? {};
|
|
1106
|
+
const fields = [];
|
|
1107
|
+
const textField = (key, label, initial) => {
|
|
1108
|
+
const input = make("input", "dsh-tw-settings-input");
|
|
1109
|
+
input.value = initial;
|
|
1110
|
+
const wrap = make("label", "dsh-tw-settings-field");
|
|
1111
|
+
wrap.append(make("span", "dsh-tw-settings-label", label), input);
|
|
1112
|
+
fields.push({
|
|
1113
|
+
key,
|
|
1114
|
+
input,
|
|
1115
|
+
initial,
|
|
1116
|
+
read: () => input.value.trim(),
|
|
1117
|
+
changed: () => input.value.trim() !== initial
|
|
1118
|
+
});
|
|
1119
|
+
section.append(wrap);
|
|
1120
|
+
};
|
|
1121
|
+
const checkField = (key, label, initial) => {
|
|
1122
|
+
const input = make("input", "dsh-tw-settings-check");
|
|
1123
|
+
input.type = "checkbox";
|
|
1124
|
+
input.checked = initial;
|
|
1125
|
+
const wrap = make("label", "dsh-tw-settings-field dsh-tw-settings-field-check");
|
|
1126
|
+
wrap.append(input, make("span", "dsh-tw-settings-label", label));
|
|
1127
|
+
fields.push({
|
|
1128
|
+
key,
|
|
1129
|
+
input,
|
|
1130
|
+
initial,
|
|
1131
|
+
read: () => input.checked,
|
|
1132
|
+
changed: () => input.checked !== initial
|
|
1133
|
+
});
|
|
1134
|
+
section.append(wrap);
|
|
1135
|
+
};
|
|
1136
|
+
const numField = (key, label, initial) => {
|
|
1137
|
+
const input = make("input", "dsh-tw-settings-input");
|
|
1138
|
+
input.type = "number";
|
|
1139
|
+
input.value = String(initial);
|
|
1140
|
+
const wrap = make("label", "dsh-tw-settings-field");
|
|
1141
|
+
wrap.append(make("span", "dsh-tw-settings-label", label), input);
|
|
1142
|
+
fields.push({
|
|
1143
|
+
key,
|
|
1144
|
+
input,
|
|
1145
|
+
initial,
|
|
1146
|
+
read: () => Number(input.value) || initial,
|
|
1147
|
+
changed: () => (Number(input.value) || initial) !== initial
|
|
1148
|
+
});
|
|
1149
|
+
section.append(wrap);
|
|
1150
|
+
};
|
|
1151
|
+
textField("note.tag", "快速笔记默认 tag", typeof note.tag === "string" ? note.tag : "inbox");
|
|
1152
|
+
checkField("git.autoCommit", "自动 commit(防抖)", git.autoCommit !== false);
|
|
1153
|
+
numField("git.debounceMs", "自动 commit 防抖(ms)", typeof git.debounceMs === "number" ? git.debounceMs : 6e4);
|
|
1154
|
+
textField("git.remote", "git 远端(空=仅本地)", typeof git.remote === "string" ? git.remote : "");
|
|
1155
|
+
textField("git.branch", "git 分支", typeof git.branch === "string" ? git.branch : "main");
|
|
1156
|
+
const save = make("button", "dsh-tw-settings-btn dsh-tw-settings-primary", "保存配置");
|
|
1157
|
+
save.type = "button";
|
|
1158
|
+
save.addEventListener("click", () => {
|
|
1159
|
+
save.disabled = true;
|
|
1160
|
+
(async () => {
|
|
1161
|
+
const patch = {};
|
|
1162
|
+
for (const field of fields) {
|
|
1163
|
+
if (!field.changed()) continue;
|
|
1164
|
+
const parts = field.key.split(".");
|
|
1165
|
+
if (parts.length === 1) {
|
|
1166
|
+
const key = parts[0];
|
|
1167
|
+
if (key !== void 0) patch[key] = field.read();
|
|
1168
|
+
} else {
|
|
1169
|
+
const [top, rest] = parts;
|
|
1170
|
+
const obj = patch[top] ?? {};
|
|
1171
|
+
obj[rest] = field.read();
|
|
1172
|
+
patch[top] = obj;
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
try {
|
|
1176
|
+
await fetchJson(CONFIG_ENDPOINT, {
|
|
1177
|
+
method: "POST",
|
|
1178
|
+
headers: { "content-type": "application/json" },
|
|
1179
|
+
body: JSON.stringify(patch)
|
|
1180
|
+
});
|
|
1181
|
+
toast("配置已保存");
|
|
1182
|
+
refresh();
|
|
1183
|
+
} catch (err) {
|
|
1184
|
+
toast(`保存失败:${err instanceof Error ? err.message : String(err)}`);
|
|
1185
|
+
} finally {
|
|
1186
|
+
save.disabled = false;
|
|
1187
|
+
}
|
|
1188
|
+
})();
|
|
1189
|
+
});
|
|
1190
|
+
section.append(save);
|
|
1191
|
+
body.append(section);
|
|
1192
|
+
}
|
|
1193
|
+
/** Plugin/theme manager: checkboxes/radios + apply (writes info, restarts). */
|
|
1194
|
+
function renderCatalogSection(body, info, catalog, refresh) {
|
|
1195
|
+
const plugins = catalog?.plugins ?? [];
|
|
1196
|
+
const themes = catalog?.themes ?? [];
|
|
1197
|
+
const languages = catalog?.languages ?? [];
|
|
1198
|
+
const activePlugins = new Set(info?.plugins ?? []);
|
|
1199
|
+
const loadedThemes = new Set(info?.themes ?? []);
|
|
1200
|
+
const activeLanguages = new Set(info?.languages ?? []);
|
|
1201
|
+
const pluginSection = make("section", "dsh-tw-settings-section");
|
|
1202
|
+
pluginSection.append(make("h3", "dsh-tw-settings-h", "插件管理(自带官方插件)"));
|
|
1203
|
+
const search = make("input", "dsh-tw-settings-input dsh-tw-settings-search");
|
|
1204
|
+
search.placeholder = "搜索插件…";
|
|
1205
|
+
const listWrap = make("div", "dsh-tw-settings-list");
|
|
1206
|
+
const applyPlugins = make("button", "dsh-tw-settings-btn dsh-tw-settings-primary", "应用插件(重启 TW)");
|
|
1207
|
+
applyPlugins.type = "button";
|
|
1208
|
+
const renderPluginList = (needle) => {
|
|
1209
|
+
listWrap.replaceChildren();
|
|
1210
|
+
const q = needle.toLowerCase();
|
|
1211
|
+
for (const plugin of plugins) {
|
|
1212
|
+
if (q.length > 0 && !`${plugin.label} ${plugin.name} ${plugin.description}`.toLowerCase().includes(q)) continue;
|
|
1213
|
+
const input = make("input", "dsh-tw-settings-check");
|
|
1214
|
+
input.type = "checkbox";
|
|
1215
|
+
input.checked = activePlugins.has(plugin.name);
|
|
1216
|
+
input.addEventListener("change", () => {
|
|
1217
|
+
if (input.checked) activePlugins.add(plugin.name);
|
|
1218
|
+
else activePlugins.delete(plugin.name);
|
|
1219
|
+
});
|
|
1220
|
+
const label = make("label", "dsh-tw-settings-row dsh-tw-settings-plugin");
|
|
1221
|
+
const name = make("span", "dsh-tw-settings-name", plugin.label);
|
|
1222
|
+
name.title = plugin.name;
|
|
1223
|
+
const desc = make("span", "dsh-tw-settings-muted", plugin.description || plugin.name);
|
|
1224
|
+
label.append(input, name, desc);
|
|
1225
|
+
listWrap.append(label);
|
|
1226
|
+
}
|
|
1227
|
+
};
|
|
1228
|
+
search.addEventListener("input", () => renderPluginList(search.value));
|
|
1229
|
+
applyPlugins.addEventListener("click", () => {
|
|
1230
|
+
applyPlugins.disabled = true;
|
|
1231
|
+
(async () => {
|
|
1232
|
+
try {
|
|
1233
|
+
await fetchJson(INFO_ENDPOINT, {
|
|
1234
|
+
method: "POST",
|
|
1235
|
+
headers: { "content-type": "application/json" },
|
|
1236
|
+
body: JSON.stringify({ plugins: [...activePlugins] })
|
|
1237
|
+
});
|
|
1238
|
+
toast("插件已应用,TW 已重启");
|
|
1239
|
+
refresh();
|
|
1240
|
+
} catch (err) {
|
|
1241
|
+
toast(`应用失败:${err instanceof Error ? err.message : String(err)}`);
|
|
1242
|
+
applyPlugins.disabled = false;
|
|
1243
|
+
}
|
|
1244
|
+
})();
|
|
1245
|
+
});
|
|
1246
|
+
renderPluginList("");
|
|
1247
|
+
pluginSection.append(search, listWrap, applyPlugins);
|
|
1248
|
+
body.append(pluginSection);
|
|
1249
|
+
const themeSection = make("section", "dsh-tw-settings-section");
|
|
1250
|
+
themeSection.append(make("h3", "dsh-tw-settings-h", "主题管理(自带主题)"));
|
|
1251
|
+
const themeHint = make("div", "dsh-tw-settings-muted", "「加载」= TW 里可用的主题(可多选,依赖链自动带上,如 heavier 会带 snowwhite+vanilla);「活动」= 当前视觉主题(单选,自动加入加载集)。应用后重启 TW。");
|
|
1252
|
+
const themeHead = make("div", "dsh-tw-settings-row dsh-tw-settings-head");
|
|
1253
|
+
themeHead.append(make("span", "dsh-tw-settings-col", "加载"), make("span", "dsh-tw-settings-col", "活动"), make("span", "dsh-tw-settings-name", "主题"));
|
|
1254
|
+
const themeList = info?.themes ?? [];
|
|
1255
|
+
let activeThemeName = themeList.length > 0 ? themeList[themeList.length - 1] : "tiddlywiki/vanilla";
|
|
1256
|
+
const themeWrap = make("div", "dsh-tw-settings-list");
|
|
1257
|
+
for (const theme of themes) {
|
|
1258
|
+
const load = make("input", "dsh-tw-settings-check");
|
|
1259
|
+
load.type = "checkbox";
|
|
1260
|
+
load.checked = loadedThemes.has(theme.name);
|
|
1261
|
+
load.title = "加载该主题";
|
|
1262
|
+
load.addEventListener("change", () => {
|
|
1263
|
+
if (load.checked) loadedThemes.add(theme.name);
|
|
1264
|
+
else loadedThemes.delete(theme.name);
|
|
1265
|
+
});
|
|
1266
|
+
const act = make("input", "dsh-tw-settings-check");
|
|
1267
|
+
act.type = "radio";
|
|
1268
|
+
act.name = "dsh-tw-active-theme";
|
|
1269
|
+
act.checked = theme.name === activeThemeName;
|
|
1270
|
+
act.title = "设为活动主题";
|
|
1271
|
+
act.addEventListener("change", () => {
|
|
1272
|
+
if (act.checked) activeThemeName = theme.name;
|
|
1273
|
+
});
|
|
1274
|
+
const name = make("span", "dsh-tw-settings-name", theme.label);
|
|
1275
|
+
name.title = theme.name;
|
|
1276
|
+
const desc = make("span", "dsh-tw-settings-muted", theme.description || theme.name);
|
|
1277
|
+
const row = make("div", "dsh-tw-settings-row dsh-tw-settings-plugin");
|
|
1278
|
+
row.append(load, act, name, desc);
|
|
1279
|
+
themeWrap.append(row);
|
|
1280
|
+
}
|
|
1281
|
+
const applyThemes = make("button", "dsh-tw-settings-btn dsh-tw-settings-primary", "应用主题(重启 TW)");
|
|
1282
|
+
applyThemes.type = "button";
|
|
1283
|
+
applyThemes.addEventListener("click", () => {
|
|
1284
|
+
applyThemes.disabled = true;
|
|
1285
|
+
(async () => {
|
|
1286
|
+
try {
|
|
1287
|
+
await fetchJson(INFO_ENDPOINT, {
|
|
1288
|
+
method: "POST",
|
|
1289
|
+
headers: { "content-type": "application/json" },
|
|
1290
|
+
body: JSON.stringify({
|
|
1291
|
+
themes: [...loadedThemes],
|
|
1292
|
+
themeActive: activeThemeName
|
|
1293
|
+
})
|
|
1294
|
+
});
|
|
1295
|
+
toast("主题已应用,TW 已重启");
|
|
1296
|
+
refresh();
|
|
1297
|
+
} catch (err) {
|
|
1298
|
+
toast(`应用失败:${err instanceof Error ? err.message : String(err)}`);
|
|
1299
|
+
applyThemes.disabled = false;
|
|
1300
|
+
}
|
|
1301
|
+
})();
|
|
1302
|
+
});
|
|
1303
|
+
themeSection.append(themeHint, themeHead, themeWrap, applyThemes);
|
|
1304
|
+
body.append(themeSection);
|
|
1305
|
+
const langSection = make("section", "dsh-tw-settings-section");
|
|
1306
|
+
langSection.append(make("h3", "dsh-tw-settings-h", "语言管理(自带官方语言包)"));
|
|
1307
|
+
const langHint = make("div", "dsh-tw-settings-muted", "勾选启用语言插件并重启 TW;如中文请选 zh-Hans(简体)或 zh-CN。");
|
|
1308
|
+
const langWrap = make("div", "dsh-tw-settings-list");
|
|
1309
|
+
for (const lang of languages) {
|
|
1310
|
+
const input = make("input", "dsh-tw-settings-check");
|
|
1311
|
+
input.type = "checkbox";
|
|
1312
|
+
input.checked = activeLanguages.has(lang.name);
|
|
1313
|
+
input.addEventListener("change", () => {
|
|
1314
|
+
if (input.checked) activeLanguages.add(lang.name);
|
|
1315
|
+
else activeLanguages.delete(lang.name);
|
|
1316
|
+
});
|
|
1317
|
+
const label = make("label", "dsh-tw-settings-row dsh-tw-settings-plugin");
|
|
1318
|
+
const name = make("span", "dsh-tw-settings-name", lang.label);
|
|
1319
|
+
name.title = lang.name;
|
|
1320
|
+
const desc = make("span", "dsh-tw-settings-muted", lang.description || lang.name);
|
|
1321
|
+
label.append(input, name, desc);
|
|
1322
|
+
langWrap.append(label);
|
|
1323
|
+
}
|
|
1324
|
+
const applyLangs = make("button", "dsh-tw-settings-btn dsh-tw-settings-primary", "应用语言(重启 TW)");
|
|
1325
|
+
applyLangs.type = "button";
|
|
1326
|
+
applyLangs.addEventListener("click", () => {
|
|
1327
|
+
applyLangs.disabled = true;
|
|
1328
|
+
(async () => {
|
|
1329
|
+
try {
|
|
1330
|
+
await fetchJson(INFO_ENDPOINT, {
|
|
1331
|
+
method: "POST",
|
|
1332
|
+
headers: { "content-type": "application/json" },
|
|
1333
|
+
body: JSON.stringify({ languages: [...activeLanguages] })
|
|
1334
|
+
});
|
|
1335
|
+
toast("语言已应用,TW 已重启");
|
|
1336
|
+
refresh();
|
|
1337
|
+
} catch (err) {
|
|
1338
|
+
toast(`应用失败:${err instanceof Error ? err.message : String(err)}`);
|
|
1339
|
+
applyLangs.disabled = false;
|
|
1340
|
+
}
|
|
1341
|
+
})();
|
|
1342
|
+
});
|
|
1343
|
+
langSection.append(langHint, langWrap, applyLangs);
|
|
1344
|
+
body.append(langSection);
|
|
1345
|
+
}
|
|
1346
|
+
function renderMain(body, state, refresh) {
|
|
1347
|
+
body.replaceChildren();
|
|
1348
|
+
renderConfigSection(body, state.config ?? {}, refresh);
|
|
1349
|
+
renderCatalogSection(body, state.info, state.catalog, refresh);
|
|
1350
|
+
}
|
|
1351
|
+
/** React wrapper consumed by the shell's settings.section slot. */
|
|
1352
|
+
function SettingsSection() {
|
|
1353
|
+
const ref = react.useRef(null);
|
|
1354
|
+
react.useEffect(() => {
|
|
1355
|
+
const el = ref.current;
|
|
1356
|
+
return el === null ? void 0 : mountSettingsPage(el);
|
|
1357
|
+
}, []);
|
|
1358
|
+
return react.createElement("div", { ref });
|
|
1359
|
+
}
|
|
1360
|
+
//#endregion
|
|
1361
|
+
//#region src/client/index.ts
|
|
1362
|
+
/**
|
|
1363
|
+
* Browser half entry for dsh-tiddlywiki (design doc §12, D5): injects the
|
|
1364
|
+
* stylesheet, mounts the sidebar entry, the center-column TiddlyWiki panel,
|
|
1365
|
+
* the floating quick-note widget, and registers the plugin's settings page
|
|
1366
|
+
* (config panel, §13) into the shell's Settings.
|
|
1367
|
+
*
|
|
1368
|
+
* Failure policy: DOM mounting problems are logged, never thrown — the web
|
|
1369
|
+
* shell fails the whole boot when a plugin apply throws.
|
|
1370
|
+
*
|
|
1371
|
+
* Export shape: `name` / `inject` / `apply`, no default.
|
|
1372
|
+
*
|
|
1373
|
+
* @module dsh-tiddlywiki/client
|
|
1374
|
+
*/
|
|
1375
|
+
/** Client plugin name. */
|
|
1376
|
+
const name = "dsh-tiddlywiki/client";
|
|
1377
|
+
/** Required client services: the slots registry (settings.section seat). */
|
|
1378
|
+
const inject = ["slots"];
|
|
1379
|
+
/**
|
|
1380
|
+
* Client entry: installs styles and mounts the DOM seats + settings page.
|
|
1381
|
+
* @param ctx - the cordis client context.
|
|
1382
|
+
*/
|
|
1383
|
+
function apply(ctx) {
|
|
1384
|
+
try {
|
|
1385
|
+
injectStyles();
|
|
1386
|
+
const state = new PanelState();
|
|
1387
|
+
const disposers = [];
|
|
1388
|
+
try {
|
|
1389
|
+
disposers.push(mountSidebarEntry(state));
|
|
1390
|
+
disposers.push(mountPanel(state));
|
|
1391
|
+
disposers.push(mountNoteWidget());
|
|
1392
|
+
disposers.push(disposeEditorPopup);
|
|
1393
|
+
} catch (error) {
|
|
1394
|
+
console.error("[dsh-tiddlywiki] mount failed:", error);
|
|
1395
|
+
}
|
|
1396
|
+
try {
|
|
1397
|
+
const removeSettings = ctx.slots?.inject("settings.section", () => ctx.slots?.register({
|
|
1398
|
+
name: "settings.section",
|
|
1399
|
+
id: "dsh-tiddlywiki",
|
|
1400
|
+
order: 50,
|
|
1401
|
+
label: "TiddlyWiki 知识库"
|
|
1402
|
+
}, SettingsSection));
|
|
1403
|
+
if (removeSettings !== void 0) disposers.push(removeSettings);
|
|
1404
|
+
} catch (error) {
|
|
1405
|
+
console.error("[dsh-tiddlywiki] settings section failed:", error);
|
|
1406
|
+
}
|
|
1407
|
+
ctx.effect?.(() => () => {
|
|
1408
|
+
for (const dispose of disposers.splice(0)) dispose();
|
|
1409
|
+
}, "dsh-tiddlywiki: client mount");
|
|
1410
|
+
} catch (error) {
|
|
1411
|
+
console.error("[dsh-tiddlywiki] client half failed to start:", error);
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
1414
|
+
//#endregion
|
|
1415
|
+
exports.apply = apply;
|
|
1416
|
+
exports.inject = inject;
|
|
1417
|
+
exports.name = name;
|