dsh-tiddlywiki 0.2.0 → 0.3.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 -21
- package/README.md +12 -6
- package/cordis.patch.yml +13 -13
- package/lib/client.bundle.js +2173 -1652
- package/lib/client.js +2183 -1662
- package/lib/index.js +2424 -2263
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
- package/src/client/editor-popup.ts +121 -121
- package/src/client/index.ts +2 -0
- package/src/client/markdown.ts +137 -0
- package/src/client/note-widget.ts +515 -373
- package/src/client/panel.ts +51 -25
- package/src/client/settings-page.ts +32 -2
- package/src/client/sidebar-entry.ts +148 -148
- package/src/client/state.ts +39 -39
- package/src/client/styles.ts +111 -28
- package/src/client/sync-button.ts +188 -0
- package/src/client/toast.ts +22 -22
- package/src/host/admin.ts +408 -408
- package/src/host/config.ts +4 -2
- package/src/host/git.ts +13 -3
- package/src/host/routes.ts +151 -1
- package/src/host/seed-notes.ts +79 -122
- package/src/host/tools.ts +28 -1
- package/src/host/tw-api.ts +157 -157
- package/src/host/wiki.ts +287 -287
- package/src/index.ts +13 -12
- package/src/sdk.ts +198 -198
package/lib/client.bundle.js
CHANGED
|
@@ -1,1652 +1,2173 @@
|
|
|
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
|
-
/* 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; }
|
|
66
|
-
|
|
67
|
-
/* ── center-column panel (fixed overlay, JS-pinned to the column rect) ── */
|
|
68
|
-
.dsh-tw-view {
|
|
69
|
-
display: none; flex-direction: column; min-height: 0; box-sizing: border-box;
|
|
70
|
-
background: var(--dsw-alias-bg-layer-1, var(--dsw-bg, #fff));
|
|
71
|
-
}
|
|
72
|
-
html[data-dsh-tw-active] .dsh-tw-view { display: flex; }
|
|
73
|
-
/* Hide the conversation content the panel overlays (all three column gens). */
|
|
74
|
-
html[data-dsh-tw-active] [data-pane="conversation"] > :not([data-dsh-tw-view]),
|
|
75
|
-
html[data-dsh-tw-active] [class*="centerCol"] > :not([data-dsh-tw-view]),
|
|
76
|
-
html[data-dsh-tw-active] .dshDesktopConversationSurface > :not([data-dsh-tw-view]) { display: none !important; }
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
}
|
|
90
|
-
.dsh-tw-panel-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
.dsh-tw-
|
|
99
|
-
.dsh-tw-status-
|
|
100
|
-
.dsh-tw-status-
|
|
101
|
-
.dsh-tw-status-
|
|
102
|
-
.dsh-tw-status-
|
|
103
|
-
|
|
104
|
-
.dsh-tw-panel-frame { flex: 1; min-height: 0; border: 0; width: 100%; display: block; background: #fff; }
|
|
105
|
-
/* [hidden] must beat the author display rules above (UA hidden is overridden). */
|
|
106
|
-
.dsh-tw-panel-frame[hidden],
|
|
107
|
-
.dsh-tw-panel-error[hidden] { display: none !important; }
|
|
108
|
-
|
|
109
|
-
.dsh-tw-panel-error {
|
|
110
|
-
flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center;
|
|
111
|
-
gap: 10px; color: var(--dsw-alias-label-secondary, #666); font-size: 13px; text-align: center; padding: 20px;
|
|
112
|
-
}
|
|
113
|
-
.dsh-tw-panel-error button {
|
|
114
|
-
border: 1px solid var(--dsw-alias-border-l2, rgba(128,128,128,.25)); background: transparent;
|
|
115
|
-
color: inherit; font: inherit; padding: 6px 14px; border-radius: 8px; cursor: pointer;
|
|
116
|
-
}
|
|
117
|
-
.dsh-tw-panel-error button:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,.12)); }
|
|
118
|
-
.dsh-tw-panel-error code { font-size: 11px; opacity: .8; max-width: 80%; overflow-wrap: anywhere; }
|
|
119
|
-
|
|
120
|
-
/* ── floating quick-note widget ────────────────────────────────
|
|
121
|
-
Positioned ABOVE the shutdown launcher FAB (fixed right:24 bottom:24,
|
|
122
|
-
z-index 900, 46px) and BELOW its confirm overlay (z-index 1000). */
|
|
123
|
-
.dsh-tw-note {
|
|
124
|
-
position: fixed; right: 24px; bottom: 88px; z-index: 950;
|
|
125
|
-
display: flex; flex-direction: column; align-items: flex-end; gap: 10px;
|
|
126
|
-
font-family: inherit;
|
|
127
|
-
}
|
|
128
|
-
.dsh-tw-note-card {
|
|
129
|
-
width: 340px; max-width: calc(100vw - 40px);
|
|
130
|
-
background: var(--dsw-alias-bg-layer-2, #fff); color: var(--dsw-alias-label-primary, #222);
|
|
131
|
-
border: 1px solid var(--dsw-alias-border-l2, rgba(0,0,0,.15));
|
|
132
|
-
border-radius: 14px; box-shadow: var(--dsw-shadow-lv3, 0 8px 30px rgba(0,0,0,.22));
|
|
133
|
-
padding: 12px; display: flex; flex-direction: column; gap: 10px;
|
|
134
|
-
font-size: 13px;
|
|
135
|
-
}
|
|
136
|
-
.dsh-tw-note-card[hidden] { display: none; }
|
|
137
|
-
.dsh-tw-note-head {
|
|
138
|
-
display: flex; align-items: center; gap: 8px;
|
|
139
|
-
font-size: 12px; font-weight: 600; color: var(--dsw-alias-label-primary, #222);
|
|
140
|
-
}
|
|
141
|
-
.dsh-tw-note-head .dsh-tw-note-label { margin-right: auto; display: flex; align-items: center; gap: 6px; }
|
|
142
|
-
.dsh-tw-note-close {
|
|
143
|
-
border: none; background: transparent; color: var(--dsw-alias-label-secondary, #888);
|
|
144
|
-
font: inherit; font-size: 15px; line-height: 1; padding: 2px 6px; border-radius: 6px; cursor: pointer;
|
|
145
|
-
}
|
|
146
|
-
.dsh-tw-note-close:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,.12)); color: var(--dsw-alias-label-primary, #222); }
|
|
147
|
-
.dsh-tw-note-fields { display: flex; gap: 8px; }
|
|
148
|
-
.dsh-tw-note-fields input {
|
|
149
|
-
flex: 1; min-width: 0; border: 1px solid var(--dsw-alias-border-l2, rgba(0,0,0,.18));
|
|
150
|
-
border-radius: 8px; padding: 6px 9px; font: inherit; font-size: 12px;
|
|
151
|
-
background: var(--dsw-alias-bg-layer-1, #fff); color: var(--dsw-alias-label-primary, inherit);
|
|
152
|
-
transition: border-color 120ms ease, box-shadow 120ms ease;
|
|
153
|
-
}
|
|
154
|
-
.dsh-tw-note-fields input:focus, .dsh-tw-note-text:focus {
|
|
155
|
-
outline: none; border-color: var(--dsw-alias-brand-primary, #3e63dd);
|
|
156
|
-
box-shadow: 0 0 0 2px color-mix(in srgb, var(--dsw-alias-brand-primary, #3e63dd) 25%, transparent);
|
|
157
|
-
}
|
|
158
|
-
.dsh-tw-note-text {
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
.dsh-tw-note-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
transition:
|
|
170
|
-
}
|
|
171
|
-
.dsh-tw-note-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
.dsh-tw-note-
|
|
183
|
-
.dsh-tw-note-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
}
|
|
191
|
-
.dsh-tw-note-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
.dsh-tw-note-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
}
|
|
205
|
-
.dsh-tw-note-
|
|
206
|
-
|
|
207
|
-
}
|
|
208
|
-
.dsh-tw-note-
|
|
209
|
-
|
|
210
|
-
.dsh-tw-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
}
|
|
219
|
-
.dsh-tw-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
}
|
|
225
|
-
.dsh-tw-
|
|
226
|
-
.dsh-tw-
|
|
227
|
-
border:
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
.dsh-tw-
|
|
232
|
-
.dsh-tw-
|
|
233
|
-
.dsh-tw-note-
|
|
234
|
-
border: 1px solid var(--dsw-alias-border-l2, rgba(0,0,0,.18));
|
|
235
|
-
background: var(--dsw-alias-bg-layer-2, #fff); color: var(--dsw-alias-label-primary, #222);
|
|
236
|
-
font: inherit; font-size:
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
.dsh-tw-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
}
|
|
251
|
-
.dsh-tw-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
.dsh-tw-
|
|
256
|
-
.dsh-tw-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
.dsh-tw-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
.dsh-tw-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
.dsh-tw-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
.dsh-tw-
|
|
286
|
-
|
|
287
|
-
background: transparent;
|
|
288
|
-
|
|
289
|
-
}
|
|
290
|
-
.dsh-tw-
|
|
291
|
-
.dsh-tw-
|
|
292
|
-
.dsh-tw-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
var
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
const
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
if (
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
const
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
}
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
const
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
}
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
if (
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
if (
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
document.
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
}
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
document.
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
if (
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
*
|
|
890
|
-
*
|
|
891
|
-
*
|
|
892
|
-
*
|
|
893
|
-
*
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
}
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
const
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
const
|
|
1049
|
-
|
|
1050
|
-
const
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
const
|
|
1054
|
-
|
|
1055
|
-
const
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
const
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
const
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
}
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
const
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
(
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
}
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
const
|
|
1337
|
-
|
|
1338
|
-
const
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
const
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
body.append(
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
const
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
const
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
}
|
|
1479
|
-
}
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
const
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
}
|
|
1586
|
-
|
|
1587
|
-
function
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
return
|
|
1592
|
-
}
|
|
1593
|
-
|
|
1594
|
-
}
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
*
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
}
|
|
1638
|
-
if (
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
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
|
+
/* 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; }
|
|
66
|
+
|
|
67
|
+
/* ── center-column panel (fixed overlay, JS-pinned to the column rect) ── */
|
|
68
|
+
.dsh-tw-view {
|
|
69
|
+
display: none; flex-direction: column; min-height: 0; box-sizing: border-box;
|
|
70
|
+
background: var(--dsw-alias-bg-layer-1, var(--dsw-bg, #fff));
|
|
71
|
+
}
|
|
72
|
+
html[data-dsh-tw-active] .dsh-tw-view { display: flex; }
|
|
73
|
+
/* Hide the conversation content the panel overlays (all three column gens). */
|
|
74
|
+
html[data-dsh-tw-active] [data-pane="conversation"] > :not([data-dsh-tw-view]),
|
|
75
|
+
html[data-dsh-tw-active] [class*="centerCol"] > :not([data-dsh-tw-view]),
|
|
76
|
+
html[data-dsh-tw-active] .dshDesktopConversationSurface > :not([data-dsh-tw-view]) { display: none !important; }
|
|
77
|
+
|
|
78
|
+
/* ── bottom-right status / reload floating buttons ───────────────────
|
|
79
|
+
The top panel bar (title + status + reload row) is gone so TW's menubar
|
|
80
|
+
reaches the panel top; status/reload now live here as floating buttons,
|
|
81
|
+
same visual language as the quick-note toggle. The ui.showPanelStatus
|
|
82
|
+
setting controls whether they mount at all. */
|
|
83
|
+
.dsh-tw-panel-status {
|
|
84
|
+
/* 状态/重载悬浮按钮:右对齐到与快速笔记按钮同一条竖线(right:24),
|
|
85
|
+
垂直排在「同步」按钮(bottom:140)上方;
|
|
86
|
+
两个按钮竖直依次排列(状态在上、重载在下),与其他插件按钮列对齐、不堆叠。 */
|
|
87
|
+
position: fixed; right: 24px; bottom: 192px; z-index: 950;
|
|
88
|
+
display: flex; flex-direction: column; align-items: flex-end; gap: 8px;
|
|
89
|
+
}
|
|
90
|
+
.dsh-tw-panel-status-btn {
|
|
91
|
+
border: 1px solid var(--dsw-alias-border-l2, rgba(0,0,0,.18));
|
|
92
|
+
background: var(--dsw-alias-bg-layer-2, #fff); color: var(--dsw-alias-label-primary, #222);
|
|
93
|
+
font: inherit; font-size: 12px; padding: 7px 12px; border-radius: 999px;
|
|
94
|
+
box-shadow: var(--dsw-shadow-lv3, 0 4px 16px rgba(0,0,0,.16)); cursor: pointer;
|
|
95
|
+
display: inline-flex; align-items: center; gap: 6px;
|
|
96
|
+
transition: background-color 120ms ease;
|
|
97
|
+
}
|
|
98
|
+
.dsh-tw-panel-status-btn:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,.1)); }
|
|
99
|
+
.dsh-tw-status-dot { width: 8px; height: 8px; border-radius: 50%; background: #999; flex: none; }
|
|
100
|
+
.dsh-tw-status-dot[data-state="running"] { background: var(--dsw-alias-state-success-primary, #3eaa5f); }
|
|
101
|
+
.dsh-tw-status-dot[data-state="starting"] { background: var(--dsw-alias-state-warning-primary, #d9822b); }
|
|
102
|
+
.dsh-tw-status-dot[data-state="failed"], .dsh-tw-status-dot[data-state="stopped"] { background: var(--dsw-alias-state-error-primary, #d13b3b); }
|
|
103
|
+
|
|
104
|
+
.dsh-tw-panel-frame { flex: 1; min-height: 0; border: 0; width: 100%; display: block; background: #fff; }
|
|
105
|
+
/* [hidden] must beat the author display rules above (UA hidden is overridden). */
|
|
106
|
+
.dsh-tw-panel-frame[hidden],
|
|
107
|
+
.dsh-tw-panel-error[hidden] { display: none !important; }
|
|
108
|
+
|
|
109
|
+
.dsh-tw-panel-error {
|
|
110
|
+
flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center;
|
|
111
|
+
gap: 10px; color: var(--dsw-alias-label-secondary, #666); font-size: 13px; text-align: center; padding: 20px;
|
|
112
|
+
}
|
|
113
|
+
.dsh-tw-panel-error button {
|
|
114
|
+
border: 1px solid var(--dsw-alias-border-l2, rgba(128,128,128,.25)); background: transparent;
|
|
115
|
+
color: inherit; font: inherit; padding: 6px 14px; border-radius: 8px; cursor: pointer;
|
|
116
|
+
}
|
|
117
|
+
.dsh-tw-panel-error button:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,.12)); }
|
|
118
|
+
.dsh-tw-panel-error code { font-size: 11px; opacity: .8; max-width: 80%; overflow-wrap: anywhere; }
|
|
119
|
+
|
|
120
|
+
/* ── floating quick-note widget ────────────────────────────────
|
|
121
|
+
Positioned ABOVE the shutdown launcher FAB (fixed right:24 bottom:24,
|
|
122
|
+
z-index 900, 46px) and BELOW its confirm overlay (z-index 1000). */
|
|
123
|
+
.dsh-tw-note {
|
|
124
|
+
position: fixed; right: 24px; bottom: 88px; z-index: 950;
|
|
125
|
+
display: flex; flex-direction: column; align-items: flex-end; gap: 10px;
|
|
126
|
+
font-family: inherit;
|
|
127
|
+
}
|
|
128
|
+
.dsh-tw-note-card {
|
|
129
|
+
width: 340px; max-width: calc(100vw - 40px);
|
|
130
|
+
background: var(--dsw-alias-bg-layer-2, #fff); color: var(--dsw-alias-label-primary, #222);
|
|
131
|
+
border: 1px solid var(--dsw-alias-border-l2, rgba(0,0,0,.15));
|
|
132
|
+
border-radius: 14px; box-shadow: var(--dsw-shadow-lv3, 0 8px 30px rgba(0,0,0,.22));
|
|
133
|
+
padding: 12px; display: flex; flex-direction: column; gap: 10px;
|
|
134
|
+
font-size: 13px;
|
|
135
|
+
}
|
|
136
|
+
.dsh-tw-note-card[hidden] { display: none; }
|
|
137
|
+
.dsh-tw-note-head {
|
|
138
|
+
display: flex; align-items: center; gap: 8px;
|
|
139
|
+
font-size: 12px; font-weight: 600; color: var(--dsw-alias-label-primary, #222);
|
|
140
|
+
}
|
|
141
|
+
.dsh-tw-note-head .dsh-tw-note-label { margin-right: auto; display: flex; align-items: center; gap: 6px; }
|
|
142
|
+
.dsh-tw-note-close {
|
|
143
|
+
border: none; background: transparent; color: var(--dsw-alias-label-secondary, #888);
|
|
144
|
+
font: inherit; font-size: 15px; line-height: 1; padding: 2px 6px; border-radius: 6px; cursor: pointer;
|
|
145
|
+
}
|
|
146
|
+
.dsh-tw-note-close:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,.12)); color: var(--dsw-alias-label-primary, #222); }
|
|
147
|
+
.dsh-tw-note-fields { display: flex; gap: 8px; }
|
|
148
|
+
.dsh-tw-note-fields input {
|
|
149
|
+
flex: 1; min-width: 0; border: 1px solid var(--dsw-alias-border-l2, rgba(0,0,0,.18));
|
|
150
|
+
border-radius: 8px; padding: 6px 9px; font: inherit; font-size: 12px;
|
|
151
|
+
background: var(--dsw-alias-bg-layer-1, #fff); color: var(--dsw-alias-label-primary, inherit);
|
|
152
|
+
transition: border-color 120ms ease, box-shadow 120ms ease;
|
|
153
|
+
}
|
|
154
|
+
.dsh-tw-note-fields input:focus, .dsh-tw-note-text:focus {
|
|
155
|
+
outline: none; border-color: var(--dsw-alias-brand-primary, #3e63dd);
|
|
156
|
+
box-shadow: 0 0 0 2px color-mix(in srgb, var(--dsw-alias-brand-primary, #3e63dd) 25%, transparent);
|
|
157
|
+
}
|
|
158
|
+
.dsh-tw-note-text::placeholder { color: var(--dsw-alias-label-dimmed, #999); }
|
|
159
|
+
/* ── Markdown editor (highlighted-textarea overlay) ────────────────
|
|
160
|
+
The <pre> highlight layer and the transparent <textarea> share identical
|
|
161
|
+
metrics (border/padding/font/line-height/white-space) so lines align 1:1. */
|
|
162
|
+
.dsh-tw-note-editor { position: relative; min-width: 0; }
|
|
163
|
+
.dsh-tw-note-editor pre,
|
|
164
|
+
.dsh-tw-note-editor textarea {
|
|
165
|
+
margin: 0; box-sizing: border-box; width: 100%;
|
|
166
|
+
border: 1px solid var(--dsw-alias-border-l2, rgba(0,0,0,.18)); border-radius: 8px;
|
|
167
|
+
padding: 8px 9px; font: inherit; font-size: 13px; line-height: 1.5; min-height: 120px;
|
|
168
|
+
white-space: pre-wrap; overflow-wrap: anywhere; word-break: break-word; tab-size: 4;
|
|
169
|
+
transition: border-color 120ms ease, box-shadow 120ms ease;
|
|
170
|
+
}
|
|
171
|
+
.dsh-tw-note-editor .dsh-tw-note-hl {
|
|
172
|
+
position: absolute; top: 0; left: 0; right: 0; bottom: 0; z-index: 0;
|
|
173
|
+
background: var(--dsw-alias-bg-layer-1, #fff); color: var(--dsw-alias-label-primary, #222);
|
|
174
|
+
pointer-events: none; overflow: hidden;
|
|
175
|
+
}
|
|
176
|
+
.dsh-tw-note-editor .dsh-tw-note-text {
|
|
177
|
+
position: relative; z-index: 1; resize: vertical;
|
|
178
|
+
background: transparent; color: transparent; caret-color: var(--dsw-alias-label-primary, #222);
|
|
179
|
+
overflow: auto;
|
|
180
|
+
}
|
|
181
|
+
.dsh-tw-note-editor .dsh-tw-note-text:focus { outline: none; }
|
|
182
|
+
.dsh-tw-note-drop .dsh-tw-note-text,
|
|
183
|
+
.dsh-tw-note-drop .dsh-tw-note-hl {
|
|
184
|
+
border-color: var(--dsw-alias-brand-primary, #3e63dd);
|
|
185
|
+
box-shadow: 0 0 0 2px color-mix(in srgb, var(--dsw-alias-brand-primary, #3e63dd) 25%, transparent);
|
|
186
|
+
}
|
|
187
|
+
/* ── markdown highlight tokens (colors only — never size/height, so the
|
|
188
|
+
overlay keeps line-for-line alignment with the invisible textarea) ── */
|
|
189
|
+
.dsh-tw-note-hl .md-heading { font-weight: 700; color: var(--dsw-alias-brand-primary, #3e63dd); }
|
|
190
|
+
.dsh-tw-note-hl .md-hash { font-weight: 700; color: color-mix(in srgb, var(--dsw-alias-brand-primary, #3e63dd) 55%, transparent); }
|
|
191
|
+
.dsh-tw-note-hl .md-code,
|
|
192
|
+
.dsh-tw-note-hl .md-code-block,
|
|
193
|
+
.dsh-tw-note-hl .md-fence {
|
|
194
|
+
font-family: ui-monospace, "Cascadia Mono", Consolas, "SF Mono", Menlo, monospace;
|
|
195
|
+
}
|
|
196
|
+
.dsh-tw-note-hl .md-code {
|
|
197
|
+
background: color-mix(in srgb, var(--dsw-alias-label-secondary, #888) 14%, transparent);
|
|
198
|
+
border-radius: 4px; padding: 0 3px;
|
|
199
|
+
}
|
|
200
|
+
.dsh-tw-note-hl .md-code-block,
|
|
201
|
+
.dsh-tw-note-hl .md-fence {
|
|
202
|
+
display: block; background: color-mix(in srgb, var(--dsw-alias-label-secondary, #888) 8%, transparent);
|
|
203
|
+
}
|
|
204
|
+
.dsh-tw-note-hl .md-bold { font-weight: 700; }
|
|
205
|
+
.dsh-tw-note-hl .md-italic { font-style: italic; }
|
|
206
|
+
.dsh-tw-note-hl .md-strike { text-decoration: line-through; opacity: .75; }
|
|
207
|
+
.dsh-tw-note-hl .md-link { color: var(--dsw-alias-brand-primary, #3e63dd); text-decoration: underline; }
|
|
208
|
+
.dsh-tw-note-hl .md-url { color: color-mix(in srgb, var(--dsw-alias-brand-primary, #3e63dd) 70%, #999); text-decoration: underline dotted; }
|
|
209
|
+
.dsh-tw-note-hl .md-image { color: var(--dsw-alias-state-success-primary, #3eaa5f); font-weight: 600; }
|
|
210
|
+
.dsh-tw-note-hl .md-bullet { font-weight: 700; color: var(--dsw-alias-brand-primary, #3e63dd); }
|
|
211
|
+
.dsh-tw-note-hl .md-number { font-weight: 700; color: var(--dsw-alias-state-warning-primary, #d9822b); }
|
|
212
|
+
.dsh-tw-note-hl .md-quote { font-style: italic; color: var(--dsw-alias-label-secondary, #888); }
|
|
213
|
+
.dsh-tw-note-hl .md-hr { color: var(--dsw-alias-label-dimmed, #999); text-decoration: line-through; }
|
|
214
|
+
.dsh-tw-note-hl .md-task { color: var(--dsw-alias-label-secondary, #888); }
|
|
215
|
+
.dsh-tw-note-hl .md-task-checked { color: var(--dsw-alias-state-success-primary, #3eaa5f); font-weight: 600; }
|
|
216
|
+
.dsh-tw-note-foot { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
|
|
217
|
+
.dsh-tw-note-foot-left, .dsh-tw-note-foot-right { display: flex; align-items: center; gap: 8px; }
|
|
218
|
+
.dsh-tw-note-hint { font-size: 11px; color: var(--dsw-alias-label-dimmed, #999); }
|
|
219
|
+
.dsh-tw-note-upload {
|
|
220
|
+
border: 1px solid var(--dsw-alias-border-l2, rgba(0,0,0,.18));
|
|
221
|
+
background: var(--dsw-alias-bg-layer-2, #fff); color: var(--dsw-alias-label-primary, #222);
|
|
222
|
+
font: inherit; font-size: 12px; padding: 6px 10px; border-radius: 8px; cursor: pointer;
|
|
223
|
+
transition: filter 120ms ease;
|
|
224
|
+
}
|
|
225
|
+
.dsh-tw-note-upload:hover { filter: brightness(.96); }
|
|
226
|
+
.dsh-tw-note-save {
|
|
227
|
+
border: 1px solid transparent; background: var(--dsw-alias-brand-primary, #3e63dd); color: #fff;
|
|
228
|
+
font: inherit; font-size: 12px; padding: 6px 16px; border-radius: 8px; cursor: pointer;
|
|
229
|
+
transition: filter 120ms ease;
|
|
230
|
+
}
|
|
231
|
+
.dsh-tw-note-save:hover:not(:disabled) { filter: brightness(1.08); }
|
|
232
|
+
.dsh-tw-note-save:disabled { opacity: .55; cursor: default; }
|
|
233
|
+
.dsh-tw-note-edit {
|
|
234
|
+
border: 1px solid var(--dsw-alias-border-l2, rgba(0,0,0,.18));
|
|
235
|
+
background: var(--dsw-alias-bg-layer-2, #fff); color: var(--dsw-alias-label-primary, #222);
|
|
236
|
+
font: inherit; font-size: 12px; padding: 6px 10px; border-radius: 8px; cursor: pointer;
|
|
237
|
+
transition: filter 120ms ease;
|
|
238
|
+
}
|
|
239
|
+
.dsh-tw-note-edit:hover:not(:disabled) { filter: brightness(.96); }
|
|
240
|
+
.dsh-tw-note-edit:disabled { opacity: .55; cursor: default; }
|
|
241
|
+
/* ── Multi-tag chip editor (quick-note tags) ─────────────────────────────── */
|
|
242
|
+
.dsh-tw-note-tags { position: relative; flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 4px; }
|
|
243
|
+
.dsh-tw-note-chips { display: flex; flex-wrap: wrap; gap: 4px; }
|
|
244
|
+
.dsh-tw-note-tagchip {
|
|
245
|
+
display: inline-flex; align-items: center; gap: 4px;
|
|
246
|
+
padding: 2px 6px 2px 8px; border-radius: 999px; font-size: 11px; line-height: 1.4;
|
|
247
|
+
background: color-mix(in srgb, var(--dsw-alias-brand-primary, #3e63dd) 10%, var(--dsw-alias-bg-layer-1, #fff));
|
|
248
|
+
border: 1px solid color-mix(in srgb, var(--dsw-alias-brand-primary, #3e63dd) 30%, transparent);
|
|
249
|
+
color: var(--dsw-alias-label-primary, #222);
|
|
250
|
+
}
|
|
251
|
+
.dsh-tw-note-tagchip-x {
|
|
252
|
+
cursor: pointer; font-size: 12px; line-height: 1; padding: 0 2px; border-radius: 50%;
|
|
253
|
+
color: var(--dsw-alias-label-secondary, #888);
|
|
254
|
+
}
|
|
255
|
+
.dsh-tw-note-tagchip-x:hover { color: var(--dsw-alias-state-error-primary, #d13b3b); }
|
|
256
|
+
.dsh-tw-note-taginput { width: 100%; box-sizing: border-box; }
|
|
257
|
+
.dsh-tw-note-tagsuggest {
|
|
258
|
+
position: absolute; left: 0; right: 0; top: calc(100% + 2px); z-index: 60;
|
|
259
|
+
max-height: 168px; overflow-y: auto; border-radius: 8px; padding: 4px;
|
|
260
|
+
background: var(--dsw-alias-bg-layer-2, #fff); color: var(--dsw-alias-label-primary, #222);
|
|
261
|
+
border: 1px solid var(--dsw-alias-border-l2, rgba(0,0,0,.18));
|
|
262
|
+
box-shadow: var(--dsw-shadow-lv2, 0 4px 16px rgba(0,0,0,.16));
|
|
263
|
+
font-size: 12px;
|
|
264
|
+
}
|
|
265
|
+
.dsh-tw-note-tagsuggest-item {
|
|
266
|
+
padding: 6px 9px; border-radius: 6px; cursor: pointer; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
|
267
|
+
}
|
|
268
|
+
.dsh-tw-note-tagsuggest-item:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,.12)); }
|
|
269
|
+
/* ── Native-editor popup iframe (quick-note "在 TW 中编辑") ──────────────── */
|
|
270
|
+
.dsh-tw-editor-popup {
|
|
271
|
+
position: fixed; left: 0; right: 0; top: 0; bottom: 0; margin: auto;
|
|
272
|
+
width: min(880px, 92vw); height: min(640px, 86vh);
|
|
273
|
+
display: flex; flex-direction: column; overflow: hidden;
|
|
274
|
+
background: var(--dsw-alias-bg-layer-2, #fff); color: var(--dsw-alias-label-primary, #222);
|
|
275
|
+
border: 1px solid var(--dsw-alias-border-l2, rgba(0,0,0,.2));
|
|
276
|
+
border-radius: 12px; box-shadow: 0 14px 44px rgba(0,0,0,.3);
|
|
277
|
+
z-index: 980; font-family: inherit; font-size: 13px;
|
|
278
|
+
}
|
|
279
|
+
.dsh-tw-editor-bar {
|
|
280
|
+
display: flex; align-items: center; gap: 8px; flex: 0 0 auto;
|
|
281
|
+
padding: 7px 8px 7px 14px; cursor: move; user-select: none;
|
|
282
|
+
background: var(--dsw-alias-bg-layer-1, #f4f5f7);
|
|
283
|
+
border-bottom: 1px solid var(--dsw-alias-border-l1, rgba(0,0,0,.1));
|
|
284
|
+
}
|
|
285
|
+
.dsh-tw-editor-title { flex: 1; min-width: 0; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
286
|
+
.dsh-tw-editor-close {
|
|
287
|
+
border: none; background: transparent; cursor: pointer; font-size: 14px; line-height: 1;
|
|
288
|
+
color: var(--dsw-alias-label-dimmed, #888); padding: 5px 8px; border-radius: 6px;
|
|
289
|
+
}
|
|
290
|
+
.dsh-tw-editor-close:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,.12)); color: var(--dsw-alias-label-primary, #222); }
|
|
291
|
+
.dsh-tw-editor-frame { flex: 1; min-height: 0; width: 100%; border: 0; background: #fff; }
|
|
292
|
+
.dsh-tw-editor-resize { position: absolute; right: 0; bottom: 0; width: 16px; height: 16px; cursor: nwse-resize; }
|
|
293
|
+
.dsh-tw-note-toggle {
|
|
294
|
+
border: 1px solid var(--dsw-alias-border-l2, rgba(0,0,0,.18));
|
|
295
|
+
background: var(--dsw-alias-bg-layer-2, #fff); color: var(--dsw-alias-label-primary, #222);
|
|
296
|
+
font: inherit; font-size: 13px; padding: 9px 16px; border-radius: 999px;
|
|
297
|
+
box-shadow: var(--dsw-shadow-lv3, 0 4px 16px rgba(0,0,0,.16)); cursor: pointer;
|
|
298
|
+
display: inline-flex; align-items: center; gap: 6px;
|
|
299
|
+
transition: background-color 120ms ease;
|
|
300
|
+
}
|
|
301
|
+
.dsh-tw-note-toggle:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,.1)); }
|
|
302
|
+
|
|
303
|
+
/* ── floating sync button (bottom-right) ───────────────────────
|
|
304
|
+
Pill FAB between the quick-note toggle (bottom:88) and the TW panel
|
|
305
|
+
status/reload floaters (bottom:192); dot reflects git state. */
|
|
306
|
+
.dsh-tw-sync {
|
|
307
|
+
position: fixed; right: 24px; bottom: 140px; z-index: 950;
|
|
308
|
+
display: inline-flex; align-items: center; gap: 7px;
|
|
309
|
+
padding: 9px 14px; border-radius: 999px;
|
|
310
|
+
border: 1px solid var(--dsw-alias-border-l2, rgba(0,0,0,.18));
|
|
311
|
+
background: var(--dsw-alias-bg-layer-2, #fff); color: var(--dsw-alias-label-primary, #222);
|
|
312
|
+
font: inherit; font-size: 13px; box-shadow: var(--dsw-shadow-lv3, 0 4px 16px rgba(0,0,0,.16));
|
|
313
|
+
cursor: pointer; transition: background-color 120ms ease;
|
|
314
|
+
}
|
|
315
|
+
.dsh-tw-sync:hover:not(:disabled) { background: var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,.1)); }
|
|
316
|
+
.dsh-tw-sync:disabled { opacity: .65; cursor: default; }
|
|
317
|
+
.dsh-tw-sync-icon { display: inline-flex; flex: none; }
|
|
318
|
+
.dsh-tw-sync-spin .dsh-tw-sync-icon svg { animation: dsh-tw-spin 1s linear infinite; transform-origin: center; }
|
|
319
|
+
@keyframes dsh-tw-spin { to { transform: rotate(360deg); } }
|
|
320
|
+
.dsh-tw-sync-dot { width: 8px; height: 8px; border-radius: 50%; background: #999; flex: none; }
|
|
321
|
+
.dsh-tw-sync-dot[data-state="clean"] { background: var(--dsw-alias-state-success-primary, #3eaa5f); }
|
|
322
|
+
.dsh-tw-sync-dot[data-state="dirty"] { background: var(--dsw-alias-state-warning-primary, #d9822b); }
|
|
323
|
+
.dsh-tw-sync-dot[data-state="behind"] { background: var(--dsw-alias-state-error-primary, #d13b3b); }
|
|
324
|
+
.dsh-tw-sync-dot[data-state="syncing"] { background: var(--dsw-alias-brand-primary, #3e63dd); }
|
|
325
|
+
|
|
326
|
+
/* ── toast ──────────────────────────────────────────────────── */
|
|
327
|
+
.dsh-tw-toast {
|
|
328
|
+
position: fixed; left: 50%; bottom: 96px; transform: translateX(-50%);
|
|
329
|
+
background: var(--dsw-alias-bg-inverse, rgba(30,30,30,.92)); color: var(--dsw-alias-label-inverse, #fff);
|
|
330
|
+
font-size: 13px; padding: 8px 16px; border-radius: 999px; z-index: 10001;
|
|
331
|
+
opacity: 0; transition: opacity .2s ease; pointer-events: none;
|
|
332
|
+
max-width: 80vw; overflow-wrap: anywhere;
|
|
333
|
+
}
|
|
334
|
+
.dsh-tw-toast.dsh-tw-toast-show { opacity: 1; }
|
|
335
|
+
|
|
336
|
+
/* ── settings page (config panel §13) ────────────────────────── */
|
|
337
|
+
.dsh-tw-settings { display: flex; flex-direction: column; gap: 10px; padding: 12px 16px; min-width: 0; }
|
|
338
|
+
.dsh-tw-settings-row { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
|
|
339
|
+
.dsh-tw-settings-chip {
|
|
340
|
+
font-size: 12px; padding: 2px 10px; border-radius: 999px;
|
|
341
|
+
border: 1px solid var(--dsw-alias-border-l2, rgba(128,128,128,.25));
|
|
342
|
+
}
|
|
343
|
+
.dsh-tw-settings-chip[data-state="running"] { color: var(--dsw-alias-state-success-primary, #3eaa5f); }
|
|
344
|
+
.dsh-tw-settings-chip[data-state="starting"] { color: var(--dsw-alias-state-warning-primary, #d9822b); }
|
|
345
|
+
.dsh-tw-settings-chip[data-state="failed"], .dsh-tw-settings-chip[data-state="stopped"] { color: var(--dsw-alias-state-error-primary, #d13b3b); }
|
|
346
|
+
.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)); }
|
|
347
|
+
.dsh-tw-settings-h { margin: 0; font-size: 13px; font-weight: 600; color: var(--dsw-alias-label-primary, #222); }
|
|
348
|
+
.dsh-tw-settings-field { display: flex; align-items: center; gap: 8px; font-size: 12px; color: var(--dsw-alias-label-secondary, inherit); }
|
|
349
|
+
.dsh-tw-settings-field-check { cursor: pointer; }
|
|
350
|
+
.dsh-tw-settings-label { flex: 0 0 170px; }
|
|
351
|
+
.dsh-tw-settings-input {
|
|
352
|
+
flex: 1; min-width: 0; font: inherit; font-size: 12px; padding: 4px 8px; border-radius: 6px;
|
|
353
|
+
border: 1px solid var(--dsw-alias-border-l2, rgba(128,128,128,.28));
|
|
354
|
+
background: var(--dsw-alias-bg-input, transparent); color: var(--dsw-alias-label-primary, #222);
|
|
355
|
+
}
|
|
356
|
+
.dsh-tw-settings-input:focus {
|
|
357
|
+
outline: none; border-color: var(--dsw-alias-brand-primary, #3e63dd);
|
|
358
|
+
box-shadow: 0 0 0 3px color-mix(in srgb, var(--dsw-alias-brand-primary, #3e63dd) 22%, transparent);
|
|
359
|
+
}
|
|
360
|
+
.dsh-tw-settings-list { display: flex; flex-direction: column; gap: 2px; max-height: 240px; overflow: auto; }
|
|
361
|
+
.dsh-tw-settings-plugin { display: flex; align-items: center; gap: 8px; padding: 3px 4px; border-radius: 6px; font-size: 12px; }
|
|
362
|
+
.dsh-tw-settings-plugin:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,.08)); }
|
|
363
|
+
.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; }
|
|
364
|
+
.dsh-tw-settings-head { padding: 2px 4px 4px; font-size: 11px; }
|
|
365
|
+
.dsh-tw-settings-col { flex: 0 0 14px; text-align: center; color: var(--dsw-alias-label-dimmed, #999); }
|
|
366
|
+
.dsh-tw-settings-row.dsh-tw-settings-plugin input { flex: 0 0 auto; margin: 0; }
|
|
367
|
+
.dsh-tw-settings-muted { color: var(--dsw-alias-label-dimmed, #999); font-size: 12px; }
|
|
368
|
+
.dsh-tw-settings-btn {
|
|
369
|
+
align-self: flex-start; border: 1px solid var(--dsw-alias-border-l2, rgba(128,128,128,.28));
|
|
370
|
+
background: transparent; color: var(--dsw-alias-label-primary, #222);
|
|
371
|
+
font: inherit; font-size: 12px; padding: 4px 12px; border-radius: 7px; cursor: pointer;
|
|
372
|
+
}
|
|
373
|
+
.dsh-tw-settings-btn:hover:not(:disabled) { background: var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,.1)); }
|
|
374
|
+
.dsh-tw-settings-btn:disabled { opacity: .55; cursor: default; }
|
|
375
|
+
.dsh-tw-settings-btn.dsh-tw-settings-primary {
|
|
376
|
+
background: var(--dsw-alias-brand-primary, #3e63dd); border-color: transparent; color: #fff;
|
|
377
|
+
}
|
|
378
|
+
.dsh-tw-settings-error { color: var(--dsw-alias-state-error-primary, #d13b3b); font-size: 12px; }
|
|
379
|
+
.dsh-tw-settings-search { flex: 0 0 auto; max-width: 220px; }
|
|
380
|
+
.dsh-tw-settings-check { accent-color: var(--dsw-alias-brand-primary, #3e63dd); }
|
|
381
|
+
`;
|
|
382
|
+
function injectStyles() {
|
|
383
|
+
if (typeof document === "undefined") return;
|
|
384
|
+
let el = document.getElementById(STYLE_ID);
|
|
385
|
+
if (el !== null) return;
|
|
386
|
+
el = document.createElement("style");
|
|
387
|
+
el.id = STYLE_ID;
|
|
388
|
+
el.dataset.plugin = "dsh-tiddlywiki";
|
|
389
|
+
el.textContent = CSS_TEXT;
|
|
390
|
+
document.head.append(el);
|
|
391
|
+
}
|
|
392
|
+
//#endregion
|
|
393
|
+
//#region src/client/state.ts
|
|
394
|
+
var PanelState = class {
|
|
395
|
+
open = false;
|
|
396
|
+
listeners = /* @__PURE__ */ new Set();
|
|
397
|
+
isOpen() {
|
|
398
|
+
return this.open;
|
|
399
|
+
}
|
|
400
|
+
toggle() {
|
|
401
|
+
this.set(!this.open);
|
|
402
|
+
}
|
|
403
|
+
openPanel() {
|
|
404
|
+
this.set(true);
|
|
405
|
+
}
|
|
406
|
+
closePanel() {
|
|
407
|
+
this.set(false);
|
|
408
|
+
}
|
|
409
|
+
set(value) {
|
|
410
|
+
if (this.open === value) return;
|
|
411
|
+
this.open = value;
|
|
412
|
+
for (const listener of [...this.listeners]) listener(value);
|
|
413
|
+
}
|
|
414
|
+
subscribe(listener) {
|
|
415
|
+
this.listeners.add(listener);
|
|
416
|
+
return () => {
|
|
417
|
+
this.listeners.delete(listener);
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
};
|
|
421
|
+
/** Inline icon: a wiki page with a TiddlyWiki-style "T" (nav-icon look). */
|
|
422
|
+
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>";
|
|
423
|
+
/** Family entries from sibling plugins, kept in a stable relative order. */
|
|
424
|
+
const FAMILY_SELECTOR = "[data-dsh-tw-entry], [data-dsh-atb-entry], [data-dsh-taskboard-entry], [data-dsh-ssh-entry]";
|
|
425
|
+
/** Find the sidebar shell root element, or undefined while not yet mounted. */
|
|
426
|
+
function sidebarRoot() {
|
|
427
|
+
const column = document.querySelector("[data-pane=\"sidebar\"], [class*=\"sidebarCol\"], .dshDesktopUpstreamSidebar, .dshDesktopSidebarSurface");
|
|
428
|
+
if (column === null) return void 0;
|
|
429
|
+
return column.querySelector("[class*=\"logoRow\"]")?.parentElement ?? column.firstElementChild;
|
|
430
|
+
}
|
|
431
|
+
/** The New Session button inside the sidebar root. */
|
|
432
|
+
function newSessionButton(root) {
|
|
433
|
+
const nested = root.querySelector("button[class*=\"newSession\"]");
|
|
434
|
+
if (nested !== null) return nested;
|
|
435
|
+
for (const child of root.children) if (child instanceof HTMLButtonElement && !child.matches("[data-dsh-tw-entry]")) return child;
|
|
436
|
+
const byAria = root.querySelector("button[aria-label=\"新建会话\"], button[aria-label=\"New Session\"], button[aria-label*=\"新会话\"], button[aria-label*=\"new session\" i]");
|
|
437
|
+
if (byAria !== null) return byAria;
|
|
438
|
+
return Array.from(root.querySelectorAll("button")).find((button) => !button.matches("[data-dsh-tw-entry]") && /新会话|新建会话|new session/i.test(button.textContent ?? ""));
|
|
439
|
+
}
|
|
440
|
+
/** Build the entry row (a detached button; insert once the shell is up). */
|
|
441
|
+
function createEntry(state) {
|
|
442
|
+
const entry = document.createElement("button");
|
|
443
|
+
entry.type = "button";
|
|
444
|
+
entry.dataset.dshTwEntry = "";
|
|
445
|
+
entry.className = "dsh-tw-entry";
|
|
446
|
+
entry.setAttribute("aria-label", "TiddlyWiki 知识库");
|
|
447
|
+
entry.innerHTML = `<span class="dsh-tw-entry-icon">${ICON}</span><span class="dsh-tw-entry-label">TiddlyWiki</span>`;
|
|
448
|
+
entry.addEventListener("click", () => {
|
|
449
|
+
state.toggle();
|
|
450
|
+
});
|
|
451
|
+
return entry;
|
|
452
|
+
}
|
|
453
|
+
/** Re-insert the entry before the whole family block (stable ordering). */
|
|
454
|
+
function placeEntry(root, entry) {
|
|
455
|
+
const button = newSessionButton(root);
|
|
456
|
+
if (button === void 0) return false;
|
|
457
|
+
if (entry.parentElement !== root) {
|
|
458
|
+
const row = button.closest("[class*=\"logoRow\"]");
|
|
459
|
+
const base = row !== null && row.parentElement === root ? row : button;
|
|
460
|
+
const family = Array.from(root.children).filter((el) => el instanceof HTMLElement && el.matches(FAMILY_SELECTOR));
|
|
461
|
+
const anchor = family.length > 0 ? family[0] ?? null : base.nextElementSibling ?? null;
|
|
462
|
+
root.insertBefore(entry, anchor);
|
|
463
|
+
}
|
|
464
|
+
return true;
|
|
465
|
+
}
|
|
466
|
+
/**
|
|
467
|
+
* Mount the sidebar entry, waiting for the shell and self-healing on later
|
|
468
|
+
* re-renders.
|
|
469
|
+
* @param state - the shared panel state the entry toggles.
|
|
470
|
+
* @returns disposer removing the entry and its observers.
|
|
471
|
+
*/
|
|
472
|
+
function mountSidebarEntry(state) {
|
|
473
|
+
const entry = createEntry(state);
|
|
474
|
+
const debug = {
|
|
475
|
+
attempts: 0,
|
|
476
|
+
found: false,
|
|
477
|
+
placed: false
|
|
478
|
+
};
|
|
479
|
+
const host = globalThis.location?.hostname;
|
|
480
|
+
if (host === "localhost" || host === "127.0.0.1") window.__twDebug = debug;
|
|
481
|
+
let root;
|
|
482
|
+
let placed = false;
|
|
483
|
+
const tryPlace = () => {
|
|
484
|
+
debug.attempts++;
|
|
485
|
+
if (root !== void 0 && !root.isConnected) {
|
|
486
|
+
rootObserver.disconnect();
|
|
487
|
+
root = void 0;
|
|
488
|
+
placed = false;
|
|
489
|
+
}
|
|
490
|
+
if (placed) {
|
|
491
|
+
if (document.body.contains(entry)) return;
|
|
492
|
+
rootObserver.disconnect();
|
|
493
|
+
root = void 0;
|
|
494
|
+
placed = false;
|
|
495
|
+
}
|
|
496
|
+
root ??= sidebarRoot();
|
|
497
|
+
if (root === void 0) return;
|
|
498
|
+
debug.found = newSessionButton(root) !== void 0;
|
|
499
|
+
placed = placeEntry(root, entry);
|
|
500
|
+
debug.placed = placed;
|
|
501
|
+
if (placed) rootObserver.observe(root, {
|
|
502
|
+
childList: true,
|
|
503
|
+
subtree: true
|
|
504
|
+
});
|
|
505
|
+
};
|
|
506
|
+
const waitObserver = new MutationObserver(() => {
|
|
507
|
+
tryPlace();
|
|
508
|
+
});
|
|
509
|
+
waitObserver.observe(document.body, {
|
|
510
|
+
childList: true,
|
|
511
|
+
subtree: true
|
|
512
|
+
});
|
|
513
|
+
const rootObserver = new MutationObserver(() => {
|
|
514
|
+
if (root === void 0 || !root.isConnected) {
|
|
515
|
+
placed = false;
|
|
516
|
+
tryPlace();
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
if (!root.contains(entry)) placed = placeEntry(root, entry);
|
|
520
|
+
});
|
|
521
|
+
const retry = setInterval(() => {
|
|
522
|
+
tryPlace();
|
|
523
|
+
}, 2e3);
|
|
524
|
+
const syncActive = () => {
|
|
525
|
+
if (state.isOpen()) entry.dataset.active = "true";
|
|
526
|
+
else delete entry.dataset.active;
|
|
527
|
+
};
|
|
528
|
+
const unsubscribe = state.subscribe(syncActive);
|
|
529
|
+
syncActive();
|
|
530
|
+
tryPlace();
|
|
531
|
+
return () => {
|
|
532
|
+
clearInterval(retry);
|
|
533
|
+
waitObserver.disconnect();
|
|
534
|
+
rootObserver.disconnect();
|
|
535
|
+
unsubscribe();
|
|
536
|
+
entry.remove();
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
//#endregion
|
|
540
|
+
//#region src/client/panel.ts
|
|
541
|
+
/**
|
|
542
|
+
* Center-column targets, most-specific shell generation first. The official
|
|
543
|
+
* layout shell (dsh-client-ui-layout) drops data-pane and uses a CSS-Module
|
|
544
|
+
* hashed `centerCol`; older shells put `data-pane="conversation"` on the same
|
|
545
|
+
* full-height grid item; DSH Desktop exposes the non-compat
|
|
546
|
+
* `.dshDesktopConversationSurface`.
|
|
547
|
+
*/
|
|
548
|
+
const COLUMN_SELECTORS = [
|
|
549
|
+
"[class*=\"centerCol\"]",
|
|
550
|
+
"[data-pane=\"conversation\"]",
|
|
551
|
+
".dshDesktopConversationSurface"
|
|
552
|
+
];
|
|
553
|
+
const ACTIVE_ATTR = "data-dsh-tw-active";
|
|
554
|
+
/** Sibling panels' activation attributes, evicted when this panel opens. */
|
|
555
|
+
const OTHER_ACTIVE_ATTRS = [
|
|
556
|
+
"data-dsh-atb-active",
|
|
557
|
+
"data-dsh-taskboard-active",
|
|
558
|
+
"data-dsh-ssh-active"
|
|
559
|
+
];
|
|
560
|
+
/** Cross-plugin activation event; detail is the activating panel name. */
|
|
561
|
+
const ACTIVATE_EVENT = "dsh-panel-activate";
|
|
562
|
+
const PANEL_NAME = "dsh-tiddlywiki";
|
|
563
|
+
/** Overlay z-index: above the shell content, below the note widget (950). */
|
|
564
|
+
const PANEL_Z_INDEX = 40;
|
|
565
|
+
/**
|
|
566
|
+
* dsh-better-sidebar's unified fixed host layer (its persistent
|
|
567
|
+
* expand/collapse toggle cluster lives inside it, at a global z-index of 25
|
|
568
|
+
* — its internal 45 is trapped by the host's stacking context). When this
|
|
569
|
+
* host is present the panel must stay BELOW it, so the sidebar toggle
|
|
570
|
+
* buttons stay visible and clickable above the full-screen TW panel.
|
|
571
|
+
*/
|
|
572
|
+
const PANEL_HOST_SELECTOR = "[data-dsh-panel-host]";
|
|
573
|
+
/** The app's own shell overlay layer (dsh-client-ui-layout pins it at 20). */
|
|
574
|
+
const APP_OVERLAY_Z_INDEX = 20;
|
|
575
|
+
/** Safety re-measure cadence for shell layout changes CSS can't see. */
|
|
576
|
+
const SYNC_INTERVAL_MS = 2e3;
|
|
577
|
+
const STATUS_ENDPOINT$2 = "/dsh-tiddlywiki/status";
|
|
578
|
+
const RESTART_ENDPOINT$1 = "/dsh-tiddlywiki/restart";
|
|
579
|
+
/**
|
|
580
|
+
* The panel's z-index: below any dsh-better-sidebar host layer so its
|
|
581
|
+
* persistent toggle cluster (top-right corner) stays visible and clickable
|
|
582
|
+
* above the full-screen TW panel, otherwise the default 40. The host's live
|
|
583
|
+
* computed z-index is read instead of hardcoding 25, so the rule tracks
|
|
584
|
+
* plugin updates; the panel is still clamped above the app's own shell
|
|
585
|
+
* overlay layer (dsh-client-ui-layout pins it at 20).
|
|
586
|
+
*/
|
|
587
|
+
function resolvePanelZIndex() {
|
|
588
|
+
const host = document.querySelector(PANEL_HOST_SELECTOR);
|
|
589
|
+
if (host === null) return PANEL_Z_INDEX;
|
|
590
|
+
const parsed = parseInt(getComputedStyle(host).zIndex, 10);
|
|
591
|
+
if (!Number.isFinite(parsed)) return PANEL_Z_INDEX;
|
|
592
|
+
return Math.max(APP_OVERLAY_Z_INDEX, Math.min(PANEL_Z_INDEX, parsed - 1));
|
|
593
|
+
}
|
|
594
|
+
function conversationColumn() {
|
|
595
|
+
for (const selector of COLUMN_SELECTORS) {
|
|
596
|
+
const el = document.querySelector(selector);
|
|
597
|
+
if (el !== null) return el;
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
async function fetchStatus$1() {
|
|
601
|
+
try {
|
|
602
|
+
const res = await fetch(STATUS_ENDPOINT$2, { signal: AbortSignal.timeout(8e3) });
|
|
603
|
+
if (!res.ok) return null;
|
|
604
|
+
return await res.json();
|
|
605
|
+
} catch {
|
|
606
|
+
return null;
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
async function requestRestart() {
|
|
610
|
+
try {
|
|
611
|
+
return (await fetch(RESTART_ENDPOINT$1, {
|
|
612
|
+
method: "POST",
|
|
613
|
+
signal: AbortSignal.timeout(8e3)
|
|
614
|
+
})).ok;
|
|
615
|
+
} catch {
|
|
616
|
+
return false;
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
function mountPanel(state) {
|
|
620
|
+
let container;
|
|
621
|
+
let columnEl;
|
|
622
|
+
let iframe;
|
|
623
|
+
let frameArea;
|
|
624
|
+
let errorArea;
|
|
625
|
+
let chip;
|
|
626
|
+
let statusDot;
|
|
627
|
+
let statusFloater;
|
|
628
|
+
let refreshTimer;
|
|
629
|
+
let refreshAttempts = 0;
|
|
630
|
+
const build = () => {
|
|
631
|
+
const view = document.createElement("div");
|
|
632
|
+
view.dataset.dshTwView = "";
|
|
633
|
+
view.className = "dsh-tw-view";
|
|
634
|
+
frameArea = document.createElement("div");
|
|
635
|
+
frameArea.className = "dsh-tw-panel-frame-wrap";
|
|
636
|
+
frameArea.style.cssText = "flex:1;min-height:0;display:flex;flex-direction:column";
|
|
637
|
+
iframe = document.createElement("iframe");
|
|
638
|
+
iframe.className = "dsh-tw-panel-frame";
|
|
639
|
+
iframe.title = "TiddlyWiki";
|
|
640
|
+
iframe.hidden = true;
|
|
641
|
+
frameArea.append(iframe);
|
|
642
|
+
errorArea = document.createElement("div");
|
|
643
|
+
errorArea.className = "dsh-tw-panel-error";
|
|
644
|
+
errorArea.hidden = true;
|
|
645
|
+
view.append(frameArea, errorArea);
|
|
646
|
+
return view;
|
|
647
|
+
};
|
|
648
|
+
/**
|
|
649
|
+
* Bottom-right floating status/reload buttons (same visual language as the
|
|
650
|
+
* quick-note toggle) — the top panel bar is gone, so TW's menubar reaches
|
|
651
|
+
* the very top of the panel and clears the host's top-right buttons. Mount
|
|
652
|
+
* is gated by `ui.showPanelStatus` (settings page toggle).
|
|
653
|
+
*/
|
|
654
|
+
const buildStatusFloater = () => {
|
|
655
|
+
const wrap = document.createElement("div");
|
|
656
|
+
wrap.className = "dsh-tw-panel-status";
|
|
657
|
+
const statusBtn = document.createElement("button");
|
|
658
|
+
statusBtn.type = "button";
|
|
659
|
+
statusBtn.className = "dsh-tw-panel-status-btn";
|
|
660
|
+
statusBtn.title = "刷新状态";
|
|
661
|
+
statusDot = document.createElement("span");
|
|
662
|
+
statusDot.className = "dsh-tw-status-dot";
|
|
663
|
+
statusDot.dataset.state = "unknown";
|
|
664
|
+
chip = document.createElement("span");
|
|
665
|
+
chip.className = "dsh-tw-panel-status-text";
|
|
666
|
+
chip.textContent = "—";
|
|
667
|
+
statusBtn.append(statusDot, chip);
|
|
668
|
+
statusBtn.addEventListener("click", () => {
|
|
669
|
+
doRefresh();
|
|
670
|
+
});
|
|
671
|
+
const reloadBtn = document.createElement("button");
|
|
672
|
+
reloadBtn.type = "button";
|
|
673
|
+
reloadBtn.className = "dsh-tw-panel-status-btn";
|
|
674
|
+
reloadBtn.textContent = "重载";
|
|
675
|
+
reloadBtn.title = "重载 TiddlyWiki 面板";
|
|
676
|
+
reloadBtn.addEventListener("click", () => {
|
|
677
|
+
if (iframe !== void 0 && !iframe.hidden) iframe.src = iframe.src;
|
|
678
|
+
});
|
|
679
|
+
wrap.append(statusBtn, reloadBtn);
|
|
680
|
+
document.body.append(wrap);
|
|
681
|
+
return wrap;
|
|
682
|
+
};
|
|
683
|
+
const setChip = (stateName, text) => {
|
|
684
|
+
if (statusDot !== void 0) statusDot.dataset.state = stateName;
|
|
685
|
+
if (chip !== void 0) chip.textContent = text;
|
|
686
|
+
};
|
|
687
|
+
/** Pin the overlay to the center column's current viewport rect. */
|
|
688
|
+
const syncRect = () => {
|
|
689
|
+
if (container === void 0 || columnEl === void 0) return;
|
|
690
|
+
const rect = columnEl.getBoundingClientRect();
|
|
691
|
+
if (rect.width === 0 && rect.height === 0) return;
|
|
692
|
+
const left = `${rect.left}px`;
|
|
693
|
+
const top = `${rect.top}px`;
|
|
694
|
+
const width = `${rect.width}px`;
|
|
695
|
+
const height = `${rect.height}px`;
|
|
696
|
+
if (container.style.left !== left) container.style.left = left;
|
|
697
|
+
if (container.style.top !== top) container.style.top = top;
|
|
698
|
+
if (container.style.width !== width) container.style.width = width;
|
|
699
|
+
if (container.style.height !== height) container.style.height = height;
|
|
700
|
+
};
|
|
701
|
+
/**
|
|
702
|
+
* Coexist with dsh-better-sidebar: keep the panel's z-index below the
|
|
703
|
+
* host layer (so its toggle cluster stays clickable above the panel) and
|
|
704
|
+
* flag the host's presence so CSS can reserve the cluster's width at the
|
|
705
|
+
* panel bar's right end. Re-run whenever the host mounts/unmounts.
|
|
706
|
+
*/
|
|
707
|
+
const applyChrome = () => {
|
|
708
|
+
if (container === void 0) return;
|
|
709
|
+
container.style.zIndex = String(resolvePanelZIndex());
|
|
710
|
+
if (document.querySelector(PANEL_HOST_SELECTOR) !== null) container.dataset.sidebarHost = "1";
|
|
711
|
+
else delete container.dataset.sidebarHost;
|
|
712
|
+
};
|
|
713
|
+
const ensure = () => {
|
|
714
|
+
if (container !== void 0) return;
|
|
715
|
+
columnEl = conversationColumn();
|
|
716
|
+
if (columnEl === void 0) return;
|
|
717
|
+
container = build();
|
|
718
|
+
container.style.position = "fixed";
|
|
719
|
+
applyChrome();
|
|
720
|
+
document.body.append(container);
|
|
721
|
+
syncRect();
|
|
722
|
+
};
|
|
723
|
+
const showError = (message) => {
|
|
724
|
+
if (iframe === void 0 || errorArea === void 0 || frameArea === void 0) return;
|
|
725
|
+
iframe.hidden = true;
|
|
726
|
+
errorArea.hidden = false;
|
|
727
|
+
errorArea.textContent = "";
|
|
728
|
+
const p = document.createElement("div");
|
|
729
|
+
p.textContent = "TiddlyWiki 服务不可用";
|
|
730
|
+
const code = document.createElement("code");
|
|
731
|
+
code.textContent = message;
|
|
732
|
+
const retry = document.createElement("button");
|
|
733
|
+
retry.type = "button";
|
|
734
|
+
retry.textContent = "重试";
|
|
735
|
+
retry.addEventListener("click", () => {
|
|
736
|
+
retry.disabled = true;
|
|
737
|
+
retry.textContent = "重启中…";
|
|
738
|
+
requestRestart().finally(() => {
|
|
739
|
+
doRefresh();
|
|
740
|
+
});
|
|
741
|
+
});
|
|
742
|
+
errorArea.append(p, code, retry);
|
|
743
|
+
};
|
|
744
|
+
const showStarting = () => {
|
|
745
|
+
if (iframe === void 0 || errorArea === void 0 || frameArea === void 0) return;
|
|
746
|
+
iframe.hidden = true;
|
|
747
|
+
errorArea.hidden = false;
|
|
748
|
+
errorArea.textContent = "";
|
|
749
|
+
const p = document.createElement("div");
|
|
750
|
+
p.textContent = "TiddlyWiki 服务正在启动…";
|
|
751
|
+
errorArea.append(p);
|
|
752
|
+
};
|
|
753
|
+
const showFrame = (url) => {
|
|
754
|
+
if (iframe === void 0 || errorArea === void 0) return;
|
|
755
|
+
errorArea.hidden = true;
|
|
756
|
+
iframe.hidden = false;
|
|
757
|
+
if (iframe.dataset.loaded !== url) {
|
|
758
|
+
iframe.dataset.loaded = url;
|
|
759
|
+
iframe.src = url;
|
|
760
|
+
}
|
|
761
|
+
};
|
|
762
|
+
const doRefresh = async () => {
|
|
763
|
+
if (refreshTimer !== void 0) {
|
|
764
|
+
window.clearTimeout(refreshTimer);
|
|
765
|
+
refreshTimer = void 0;
|
|
766
|
+
}
|
|
767
|
+
const payload = await fetchStatus$1();
|
|
768
|
+
if (payload === null) {
|
|
769
|
+
setChip("failed", "状态不可达");
|
|
770
|
+
showError("无法访问 /dsh-tiddlywiki/status");
|
|
771
|
+
return;
|
|
772
|
+
}
|
|
773
|
+
if (payload.status === "running" && typeof payload.url === "string") {
|
|
774
|
+
setChip("running", "在线");
|
|
775
|
+
refreshAttempts = 0;
|
|
776
|
+
showFrame(payload.url);
|
|
777
|
+
return;
|
|
778
|
+
}
|
|
779
|
+
if (payload.status === "starting") {
|
|
780
|
+
setChip("starting", "启动中");
|
|
781
|
+
showStarting();
|
|
782
|
+
if (refreshAttempts < 30) {
|
|
783
|
+
refreshAttempts++;
|
|
784
|
+
refreshTimer = window.setTimeout(() => {
|
|
785
|
+
doRefresh();
|
|
786
|
+
}, 1500);
|
|
787
|
+
}
|
|
788
|
+
return;
|
|
789
|
+
}
|
|
790
|
+
setChip("failed", "离线");
|
|
791
|
+
refreshAttempts = 0;
|
|
792
|
+
showError(payload.error ?? `服务状态:${payload.status}`);
|
|
793
|
+
};
|
|
794
|
+
const applyActive = () => {
|
|
795
|
+
if (state.isOpen()) {
|
|
796
|
+
for (const attr of OTHER_ACTIVE_ATTRS) document.documentElement.removeAttribute(attr);
|
|
797
|
+
document.documentElement.setAttribute(ACTIVE_ATTR, "");
|
|
798
|
+
document.dispatchEvent(new CustomEvent(ACTIVATE_EVENT, { detail: PANEL_NAME }));
|
|
799
|
+
doRefresh();
|
|
800
|
+
} else {
|
|
801
|
+
document.documentElement.removeAttribute(ACTIVE_ATTR);
|
|
802
|
+
if (refreshTimer !== void 0) {
|
|
803
|
+
window.clearTimeout(refreshTimer);
|
|
804
|
+
refreshTimer = void 0;
|
|
805
|
+
}
|
|
806
|
+
refreshAttempts = 0;
|
|
807
|
+
}
|
|
808
|
+
};
|
|
809
|
+
const onOtherActivate = (event) => {
|
|
810
|
+
if (event.detail !== PANEL_NAME && state.isOpen()) state.closePanel();
|
|
811
|
+
};
|
|
812
|
+
const onClickSidebarRow = (event) => {
|
|
813
|
+
if (!state.isOpen()) return;
|
|
814
|
+
const target = event.target;
|
|
815
|
+
if (target === null) return;
|
|
816
|
+
if (target.closest("[data-dsh-tw-entry]") !== null) return;
|
|
817
|
+
if (target.closest("[class*=\"sessionRow\"], [class*=\"projectRow\"], [class*=\"searchResultRow\"], [class*=\"searchResultWorkspace\"], [class*=\"newSession\"]") !== null) state.closePanel();
|
|
818
|
+
};
|
|
819
|
+
const waitObserver = new MutationObserver(() => {
|
|
820
|
+
ensure();
|
|
821
|
+
applyChrome();
|
|
822
|
+
});
|
|
823
|
+
waitObserver.observe(document.body, {
|
|
824
|
+
childList: true,
|
|
825
|
+
subtree: true
|
|
826
|
+
});
|
|
827
|
+
const resizeObserver = new ResizeObserver(() => syncRect());
|
|
828
|
+
resizeObserver.observe(document.body);
|
|
829
|
+
const syncInterval = window.setInterval(syncRect, SYNC_INTERVAL_MS);
|
|
830
|
+
const onWindowResize = () => syncRect();
|
|
831
|
+
window.addEventListener("resize", onWindowResize);
|
|
832
|
+
const onAnyScroll = () => syncRect();
|
|
833
|
+
window.addEventListener("scroll", onAnyScroll, true);
|
|
834
|
+
document.addEventListener("click", onClickSidebarRow, true);
|
|
835
|
+
document.addEventListener(ACTIVATE_EVENT, onOtherActivate);
|
|
836
|
+
const unsubscribe = state.subscribe(applyActive);
|
|
837
|
+
ensure();
|
|
838
|
+
applyActive();
|
|
839
|
+
(async () => {
|
|
840
|
+
if ((await fetchStatus$1())?.ui?.showPanelStatus === false) return;
|
|
841
|
+
statusFloater = buildStatusFloater();
|
|
842
|
+
})();
|
|
843
|
+
return () => {
|
|
844
|
+
if (refreshTimer !== void 0) window.clearTimeout(refreshTimer);
|
|
845
|
+
window.clearInterval(syncInterval);
|
|
846
|
+
window.removeEventListener("resize", onWindowResize);
|
|
847
|
+
window.removeEventListener("scroll", onAnyScroll, true);
|
|
848
|
+
resizeObserver.disconnect();
|
|
849
|
+
document.removeEventListener("click", onClickSidebarRow, true);
|
|
850
|
+
document.removeEventListener(ACTIVATE_EVENT, onOtherActivate);
|
|
851
|
+
waitObserver.disconnect();
|
|
852
|
+
unsubscribe();
|
|
853
|
+
document.documentElement.removeAttribute(ACTIVE_ATTR);
|
|
854
|
+
container?.remove();
|
|
855
|
+
statusFloater?.remove();
|
|
856
|
+
};
|
|
857
|
+
}
|
|
858
|
+
//#endregion
|
|
859
|
+
//#region src/client/toast.ts
|
|
860
|
+
/**
|
|
861
|
+
* One-shot transient toast, shared by the quick-note widget and the settings
|
|
862
|
+
* page. Reuses a single body-level element (`.dsh-tw-toast`) with a show
|
|
863
|
+
* class; the element is created lazily and styled by styles.ts.
|
|
864
|
+
*
|
|
865
|
+
* @module dsh-tiddlywiki/client/toast
|
|
866
|
+
*/
|
|
867
|
+
function toast(message) {
|
|
868
|
+
let el = document.querySelector(".dsh-tw-toast");
|
|
869
|
+
if (el === null) {
|
|
870
|
+
el = document.createElement("div");
|
|
871
|
+
el.className = "dsh-tw-toast";
|
|
872
|
+
document.body.append(el);
|
|
873
|
+
}
|
|
874
|
+
el.textContent = message;
|
|
875
|
+
el.classList.add("dsh-tw-toast-show");
|
|
876
|
+
clearTimeout(el.__twToastTimer);
|
|
877
|
+
el.__twToastTimer = window.setTimeout(() => {
|
|
878
|
+
el?.classList.remove("dsh-tw-toast-show");
|
|
879
|
+
}, 2500);
|
|
880
|
+
}
|
|
881
|
+
//#endregion
|
|
882
|
+
//#region src/client/editor-popup.ts
|
|
883
|
+
/**
|
|
884
|
+
* Floating popup iframe that loads TiddlyWiki's NATIVE editor for a draft
|
|
885
|
+
* (quick-note "✏️ 在 TW 中编辑"). A small draggable + resizable overlay with its
|
|
886
|
+
* own iframe pointed at `twUrl#<draftTitle>`: the fragment-only navigation
|
|
887
|
+
* triggers TW's hashchange, which opens the draft in the story, and because the
|
|
888
|
+
* draft tiddler carries `draft.of` the story renders the native EditTemplate.
|
|
889
|
+
*
|
|
890
|
+
* Independent of the center panel — a separate floating window so the user can
|
|
891
|
+
* edit a note without leaving the chat context.
|
|
892
|
+
*
|
|
893
|
+
* @module dsh-tiddlywiki/client/editor-popup
|
|
894
|
+
*/
|
|
895
|
+
let root;
|
|
896
|
+
let frame;
|
|
897
|
+
let titleEl;
|
|
898
|
+
/** Open (create on first use) the popup and load `url` (twUrl#draftTitle). */
|
|
899
|
+
function openEditorPopup(url, label) {
|
|
900
|
+
ensurePopup();
|
|
901
|
+
if (root === void 0 || frame === void 0) return;
|
|
902
|
+
if (titleEl !== void 0) titleEl.textContent = `TiddlyWiki 编辑器 · ${label}`;
|
|
903
|
+
root.style.display = "";
|
|
904
|
+
frame.src = url;
|
|
905
|
+
}
|
|
906
|
+
/** Remove the popup DOM entirely (plugin dispose). */
|
|
907
|
+
function disposeEditorPopup() {
|
|
908
|
+
root?.remove();
|
|
909
|
+
root = void 0;
|
|
910
|
+
frame = void 0;
|
|
911
|
+
titleEl = void 0;
|
|
912
|
+
}
|
|
913
|
+
function ensurePopup() {
|
|
914
|
+
if (root !== void 0 && frame !== void 0) return;
|
|
915
|
+
root = document.createElement("div");
|
|
916
|
+
root.className = "dsh-tw-editor-popup";
|
|
917
|
+
root.style.display = "none";
|
|
918
|
+
const bar = document.createElement("div");
|
|
919
|
+
bar.className = "dsh-tw-editor-bar";
|
|
920
|
+
titleEl = document.createElement("span");
|
|
921
|
+
titleEl.className = "dsh-tw-editor-title";
|
|
922
|
+
titleEl.textContent = "TiddlyWiki 编辑器";
|
|
923
|
+
const close = document.createElement("button");
|
|
924
|
+
close.type = "button";
|
|
925
|
+
close.className = "dsh-tw-editor-close";
|
|
926
|
+
close.textContent = "✕";
|
|
927
|
+
close.title = "关闭";
|
|
928
|
+
bar.append(titleEl, close);
|
|
929
|
+
frame = document.createElement("iframe");
|
|
930
|
+
frame.className = "dsh-tw-editor-frame";
|
|
931
|
+
frame.setAttribute("sandbox", "allow-scripts allow-same-origin allow-forms allow-popups");
|
|
932
|
+
frame.title = "TiddlyWiki 编辑器";
|
|
933
|
+
const resize = document.createElement("div");
|
|
934
|
+
resize.className = "dsh-tw-editor-resize";
|
|
935
|
+
resize.title = "拖拽调整大小";
|
|
936
|
+
root.append(bar, frame, resize);
|
|
937
|
+
document.body.append(root);
|
|
938
|
+
close.addEventListener("click", () => {
|
|
939
|
+
if (root !== void 0) root.style.display = "none";
|
|
940
|
+
});
|
|
941
|
+
bar.addEventListener("mousedown", (event) => {
|
|
942
|
+
if (event.button !== 0 || root === void 0) return;
|
|
943
|
+
event.preventDefault();
|
|
944
|
+
const rect = root.getBoundingClientRect();
|
|
945
|
+
const startX = event.clientX;
|
|
946
|
+
const startY = event.clientY;
|
|
947
|
+
const baseLeft = rect.left;
|
|
948
|
+
const baseTop = rect.top;
|
|
949
|
+
const onMove = (ev) => {
|
|
950
|
+
if (root === void 0) return;
|
|
951
|
+
root.style.left = `${baseLeft + ev.clientX - startX}px`;
|
|
952
|
+
root.style.top = `${baseTop + ev.clientY - startY}px`;
|
|
953
|
+
root.style.margin = "0";
|
|
954
|
+
root.style.right = "auto";
|
|
955
|
+
root.style.bottom = "auto";
|
|
956
|
+
};
|
|
957
|
+
const onUp = () => {
|
|
958
|
+
window.removeEventListener("mousemove", onMove);
|
|
959
|
+
window.removeEventListener("mouseup", onUp);
|
|
960
|
+
};
|
|
961
|
+
window.addEventListener("mousemove", onMove);
|
|
962
|
+
window.addEventListener("mouseup", onUp);
|
|
963
|
+
});
|
|
964
|
+
resize.addEventListener("mousedown", (event) => {
|
|
965
|
+
if (event.button !== 0 || root === void 0) return;
|
|
966
|
+
event.preventDefault();
|
|
967
|
+
event.stopPropagation();
|
|
968
|
+
const rect = root.getBoundingClientRect();
|
|
969
|
+
const startX = event.clientX;
|
|
970
|
+
const startY = event.clientY;
|
|
971
|
+
const baseW = rect.width;
|
|
972
|
+
const baseH = rect.height;
|
|
973
|
+
const onMove = (ev) => {
|
|
974
|
+
if (root === void 0) return;
|
|
975
|
+
root.style.width = `${Math.max(360, baseW + ev.clientX - startX)}px`;
|
|
976
|
+
root.style.height = `${Math.max(260, baseH + ev.clientY - startY)}px`;
|
|
977
|
+
root.style.right = "auto";
|
|
978
|
+
root.style.bottom = "auto";
|
|
979
|
+
};
|
|
980
|
+
const onUp = () => {
|
|
981
|
+
window.removeEventListener("mousemove", onMove);
|
|
982
|
+
window.removeEventListener("mouseup", onUp);
|
|
983
|
+
};
|
|
984
|
+
window.addEventListener("mousemove", onMove);
|
|
985
|
+
window.addEventListener("mouseup", onUp);
|
|
986
|
+
});
|
|
987
|
+
}
|
|
988
|
+
//#endregion
|
|
989
|
+
//#region src/client/markdown.ts
|
|
990
|
+
/**
|
|
991
|
+
* Lightweight Markdown syntax highlighter for the quick-note editor.
|
|
992
|
+
*
|
|
993
|
+
* The editor is the classic "highlighted textarea": a `<pre>` rendered BEHIND
|
|
994
|
+
* a transparent-text `<textarea>` with identical metrics. To keep the two
|
|
995
|
+
* layers perfectly aligned we NEVER change the user's text — every token is
|
|
996
|
+
* only wrapped in a colored `<span class="md-*">`, so each line keeps its
|
|
997
|
+
* exact characters and column widths. Leading whitespace (indentation) is
|
|
998
|
+
* preserved as-is so list/heading/quote alignment survives.
|
|
999
|
+
*
|
|
1000
|
+
* Coverage is deliberately pragmatic (a scratch-note editor, not CommonMark):
|
|
1001
|
+
* ATX headings, fenced code blocks, blockquotes, bullet + ordered + task
|
|
1002
|
+
* lists, horizontal rules, inline code, bold, italic, strikethrough, links
|
|
1003
|
+
* and images. HTML is escaped first, so the output is always safe to set as
|
|
1004
|
+
* `innerHTML` (no raw HTML from the user is ever executed).
|
|
1005
|
+
*
|
|
1006
|
+
* @module dsh-tiddlywiki/client/markdown
|
|
1007
|
+
*/
|
|
1008
|
+
/** Escape a raw source fragment for safe insertion into the overlay HTML. */
|
|
1009
|
+
function escapeHtml(value) {
|
|
1010
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
1011
|
+
}
|
|
1012
|
+
/**
|
|
1013
|
+
* Inline token highlighting on ALREADY-ESCAPED text. Inline code spans are
|
|
1014
|
+
* first pulled out into placeholders so later passes (links/bold/italic) can
|
|
1015
|
+
* never reach inside them, then restored.
|
|
1016
|
+
*/
|
|
1017
|
+
function highlightInline(escaped) {
|
|
1018
|
+
const codeTokens = [];
|
|
1019
|
+
let s = escaped.replace(/(`+)([^`]+?)\1/g, (_all, t, code) => {
|
|
1020
|
+
codeTokens.push(`<span class="md-code">${t}${code}${t}</span>`);
|
|
1021
|
+
return `\u0000${codeTokens.length - 1}\u0000`;
|
|
1022
|
+
});
|
|
1023
|
+
s = s.replace(/!\[([^\]]*)\]\(([^)\s]+)\)/g, (_a, alt, url) => `<span class="md-image"></span>`);
|
|
1024
|
+
s = s.replace(/\[([^\]]+)\]\(([^)\s]+)\)/g, (_a, text, url) => `<span class="md-link">[${text}](</span><span class="md-url">${url}</span><span class="md-link">)</span>`);
|
|
1025
|
+
const boldTokens = [];
|
|
1026
|
+
s = s.replace(/(\*\*|__)([^*_]+?)\1/g, (_a, d, text) => {
|
|
1027
|
+
boldTokens.push(`<span class="md-bold">${d}${text}${d}</span>`);
|
|
1028
|
+
return `\u0001${boldTokens.length - 1}\u0001`;
|
|
1029
|
+
});
|
|
1030
|
+
s = s.replace(/(\*|_)([^*_]+?)\1/g, "<span class=\"md-italic\">$1$2$1</span>");
|
|
1031
|
+
s = s.replace(/~~([^~]+?)~~/g, "<span class=\"md-strike\">~~$1~~</span>");
|
|
1032
|
+
return s.replace(/\u0001(\d+)\u0001/g, (_a, i) => boldTokens[Number(i)] ?? "").replace(/\u0000(\d+)\u0000/g, (_a, i) => codeTokens[Number(i)] ?? "");
|
|
1033
|
+
}
|
|
1034
|
+
/**
|
|
1035
|
+
* Match and return groups as a plain string array ('' for absent groups), or
|
|
1036
|
+
* null when the pattern does not match. Avoids noUncheckedIndexedAccess
|
|
1037
|
+
* friction on RegExpMatchArray indexing.
|
|
1038
|
+
*/
|
|
1039
|
+
function groups(re, value) {
|
|
1040
|
+
const m = value.match(re);
|
|
1041
|
+
if (m === null) return null;
|
|
1042
|
+
return Array.from(m, (g) => g ?? "");
|
|
1043
|
+
}
|
|
1044
|
+
/** Highlight one non-fence line (leading whitespace preserved verbatim). */
|
|
1045
|
+
function highlightLine(raw) {
|
|
1046
|
+
const leadMatch = groups(/^(\s*)(.*)$/, raw);
|
|
1047
|
+
const lead = leadMatch === null ? "" : leadMatch[1];
|
|
1048
|
+
const body = leadMatch === null ? raw : leadMatch[2];
|
|
1049
|
+
if (body.length === 0) return raw;
|
|
1050
|
+
const heading = groups(/^(#{1,6})(\s+)(.*)$/, body);
|
|
1051
|
+
if (heading !== null) return `${lead}<span class="md-heading">${escapeHtml(heading[1])}${escapeHtml(heading[2])}</span>${highlightInline(escapeHtml(heading[3]))}`;
|
|
1052
|
+
if (/^(-{3,}|\*{3,}|_{3,})\s*$/.test(body)) return `${lead}<span class="md-hr">${escapeHtml(body)}</span>`;
|
|
1053
|
+
const quote = groups(/^(>)( ?)(.*)$/, body);
|
|
1054
|
+
if (quote !== null) return `${lead}<span class="md-quote">${escapeHtml(quote[1])}</span>${escapeHtml(quote[2])}${highlightInline(escapeHtml(quote[3]))}`;
|
|
1055
|
+
const task = groups(/^([-*+])(\s+)(\[[ xX]\])(\s+)(.*)$/, body);
|
|
1056
|
+
if (task !== null) {
|
|
1057
|
+
const checked = /\[[xX]\]/.test(task[3]);
|
|
1058
|
+
return `${lead}<span class="md-bullet">${escapeHtml(task[1])}</span>${escapeHtml(task[2])}<span class="md-task${checked ? " md-task-checked" : ""}">${escapeHtml(task[3])}</span>${escapeHtml(task[4])}${highlightInline(escapeHtml(task[5]))}`;
|
|
1059
|
+
}
|
|
1060
|
+
const bullet = groups(/^([-*+])(\s+)(.*)$/, body);
|
|
1061
|
+
if (bullet !== null) return `${lead}<span class="md-bullet">${escapeHtml(bullet[1])}</span>${escapeHtml(bullet[2])}${highlightInline(escapeHtml(bullet[3]))}`;
|
|
1062
|
+
const ordered = groups(/^(\d+)([.)])(\s+)(.*)$/, body);
|
|
1063
|
+
if (ordered !== null) return `${lead}<span class="md-number">${escapeHtml(ordered[1])}${escapeHtml(ordered[2])}</span>${escapeHtml(ordered[3])}${highlightInline(escapeHtml(ordered[4]))}`;
|
|
1064
|
+
return `${lead}${highlightInline(escapeHtml(body))}`;
|
|
1065
|
+
}
|
|
1066
|
+
/**
|
|
1067
|
+
* Highlight full Markdown source into safe `<span>`-wrapped HTML. Line count
|
|
1068
|
+
* and per-line text (including whitespace) are preserved exactly.
|
|
1069
|
+
*/
|
|
1070
|
+
function highlightMarkdown(src) {
|
|
1071
|
+
const lines = src.split("\n");
|
|
1072
|
+
const out = [];
|
|
1073
|
+
let inFence = false;
|
|
1074
|
+
for (const rawLine of lines) {
|
|
1075
|
+
if (rawLine.match(/^\s*(```|~~~)\s*([\w-]*)\s*$/) !== null) {
|
|
1076
|
+
inFence = !inFence;
|
|
1077
|
+
out.push(`<span class="md-fence">${escapeHtml(rawLine)}</span>`);
|
|
1078
|
+
continue;
|
|
1079
|
+
}
|
|
1080
|
+
if (inFence) {
|
|
1081
|
+
out.push(`<span class="md-code-block">${escapeHtml(rawLine)}</span>`);
|
|
1082
|
+
continue;
|
|
1083
|
+
}
|
|
1084
|
+
out.push(highlightLine(rawLine));
|
|
1085
|
+
}
|
|
1086
|
+
return out.join("\n");
|
|
1087
|
+
}
|
|
1088
|
+
//#endregion
|
|
1089
|
+
//#region src/client/note-widget.ts
|
|
1090
|
+
/**
|
|
1091
|
+
* Floating quick-note widget (design doc §12, D6/D7) — bottom-right, fixed,
|
|
1092
|
+
* collapsible, independent of any shell DOM. Lets the human jot drafts /
|
|
1093
|
+
* scratch notes while waiting for the AI or drafting the next prompt.
|
|
1094
|
+
*
|
|
1095
|
+
* Save posts to /dsh-tiddlywiki/note → an independent tiddler (title & tags
|
|
1096
|
+
* editable; defaults: timestamp title + config tag, usually "inbox").
|
|
1097
|
+
*
|
|
1098
|
+
* ui.showQuickNote config: the widget is mounted async and stays hidden (and
|
|
1099
|
+
* never appended) when the option is off, so the toggle button can be disabled
|
|
1100
|
+
* from the settings page without touching the shell.
|
|
1101
|
+
*
|
|
1102
|
+
* Tag editor: multi-select chips + autocomplete from the wiki's existing tags
|
|
1103
|
+
* (GET /dsh-tiddlywiki/tags). Enter/`,` commits the typed value; Backspace on
|
|
1104
|
+
* an empty field removes the last chip; the dropdown filters on typing.
|
|
1105
|
+
*
|
|
1106
|
+
* Markdown editing: a "highlighted textarea" overlay (a `<pre>` rendered
|
|
1107
|
+
* behind a transparent-text `<textarea>`, see markdown.ts) so Markdown is
|
|
1108
|
+
* syntax-highlighted as you type — no dependencies, perfect alignment.
|
|
1109
|
+
*
|
|
1110
|
+
* File upload: the body accepts files via the 📎 button or drag-and-drop;
|
|
1111
|
+
* each file is POSTed raw to /dsh-tiddlywiki/upload (saved under the wiki's
|
|
1112
|
+
* `files/` folder, git-tracked and served at `/files/<name>`) and a Markdown
|
|
1113
|
+
* image/link line is inserted at the caret.
|
|
1114
|
+
*
|
|
1115
|
+
* @module dsh-tiddlywiki/client/note-widget
|
|
1116
|
+
*/
|
|
1117
|
+
const NOTE_ENDPOINT = "/dsh-tiddlywiki/note";
|
|
1118
|
+
const EDIT_ENDPOINT = "/dsh-tiddlywiki/edit";
|
|
1119
|
+
const STATUS_ENDPOINT$1 = "/dsh-tiddlywiki/status";
|
|
1120
|
+
const TAGS_ENDPOINT = "/dsh-tiddlywiki/tags";
|
|
1121
|
+
const UPLOAD_ENDPOINT = "/dsh-tiddlywiki/upload";
|
|
1122
|
+
const MAX_UPLOAD_BYTES = 64 * 1024 * 1024;
|
|
1123
|
+
function pad$1(n) {
|
|
1124
|
+
return n < 10 ? `0${n}` : String(n);
|
|
1125
|
+
}
|
|
1126
|
+
/** Default note title: `YYYY-MM-DD HH:mm`. */
|
|
1127
|
+
function timestampTitle(date = /* @__PURE__ */ new Date()) {
|
|
1128
|
+
return `${date.getFullYear()}-${pad$1(date.getMonth() + 1)}-${pad$1(date.getDate())} ${pad$1(date.getHours())}:${pad$1(date.getMinutes())}`;
|
|
1129
|
+
}
|
|
1130
|
+
async function fetchUiOptions() {
|
|
1131
|
+
const fallback = {
|
|
1132
|
+
showQuickNote: true,
|
|
1133
|
+
defaultTag: "inbox"
|
|
1134
|
+
};
|
|
1135
|
+
try {
|
|
1136
|
+
const res = await fetch(STATUS_ENDPOINT$1, { signal: AbortSignal.timeout(5e3) });
|
|
1137
|
+
if (!res.ok) return fallback;
|
|
1138
|
+
const payload = await res.json();
|
|
1139
|
+
return {
|
|
1140
|
+
showQuickNote: payload.ui?.showQuickNote !== false,
|
|
1141
|
+
defaultTag: payload.note?.tag ?? "inbox"
|
|
1142
|
+
};
|
|
1143
|
+
} catch {
|
|
1144
|
+
return fallback;
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
/** Multi-tag chip editor with autocomplete from the wiki's existing tags. */
|
|
1148
|
+
function buildTagEditor() {
|
|
1149
|
+
const wrap = document.createElement("div");
|
|
1150
|
+
wrap.className = "dsh-tw-note-tags";
|
|
1151
|
+
const chipWrap = document.createElement("div");
|
|
1152
|
+
chipWrap.className = "dsh-tw-note-chips";
|
|
1153
|
+
const input = document.createElement("input");
|
|
1154
|
+
input.className = "dsh-tw-note-taginput";
|
|
1155
|
+
input.placeholder = "tag(可多选,自动补全)";
|
|
1156
|
+
const suggest = document.createElement("div");
|
|
1157
|
+
suggest.className = "dsh-tw-note-tagsuggest";
|
|
1158
|
+
suggest.hidden = true;
|
|
1159
|
+
wrap.append(chipWrap, input, suggest);
|
|
1160
|
+
const chips = [];
|
|
1161
|
+
const hideSuggest = () => {
|
|
1162
|
+
suggest.hidden = true;
|
|
1163
|
+
};
|
|
1164
|
+
const renderChips = () => {
|
|
1165
|
+
chipWrap.replaceChildren();
|
|
1166
|
+
for (const tag of chips) {
|
|
1167
|
+
const chip = document.createElement("span");
|
|
1168
|
+
chip.className = "dsh-tw-note-tagchip";
|
|
1169
|
+
chip.textContent = tag;
|
|
1170
|
+
const x = document.createElement("span");
|
|
1171
|
+
x.className = "dsh-tw-note-tagchip-x";
|
|
1172
|
+
x.textContent = "×";
|
|
1173
|
+
x.title = `移除 tag「${tag}」`;
|
|
1174
|
+
x.addEventListener("click", (event) => {
|
|
1175
|
+
event.stopPropagation();
|
|
1176
|
+
const i = chips.indexOf(tag);
|
|
1177
|
+
if (i >= 0) {
|
|
1178
|
+
chips.splice(i, 1);
|
|
1179
|
+
renderChips();
|
|
1180
|
+
}
|
|
1181
|
+
});
|
|
1182
|
+
chip.append(x);
|
|
1183
|
+
chipWrap.append(chip);
|
|
1184
|
+
}
|
|
1185
|
+
};
|
|
1186
|
+
const addTag = (tag) => {
|
|
1187
|
+
const t = tag.trim();
|
|
1188
|
+
if (t.length === 0 || chips.includes(t)) return;
|
|
1189
|
+
chips.push(t);
|
|
1190
|
+
input.value = "";
|
|
1191
|
+
renderChips();
|
|
1192
|
+
hideSuggest();
|
|
1193
|
+
input.focus();
|
|
1194
|
+
};
|
|
1195
|
+
const commitInput = () => {
|
|
1196
|
+
for (const raw of input.value.split(/\s+/)) addTag(raw);
|
|
1197
|
+
};
|
|
1198
|
+
let knownTags = [];
|
|
1199
|
+
let tagsPromise;
|
|
1200
|
+
const ensureTags = () => {
|
|
1201
|
+
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(() => []);
|
|
1202
|
+
tagsPromise.then((list) => {
|
|
1203
|
+
knownTags = list;
|
|
1204
|
+
});
|
|
1205
|
+
return tagsPromise;
|
|
1206
|
+
};
|
|
1207
|
+
const showSuggest = () => {
|
|
1208
|
+
const q = input.value.trim().toLowerCase();
|
|
1209
|
+
const matches = knownTags.filter((t) => !chips.includes(t) && (q.length === 0 || t.toLowerCase().includes(q))).slice(0, 8);
|
|
1210
|
+
suggest.replaceChildren();
|
|
1211
|
+
for (const tag of matches) {
|
|
1212
|
+
const item = document.createElement("div");
|
|
1213
|
+
item.className = "dsh-tw-note-tagsuggest-item";
|
|
1214
|
+
item.textContent = tag;
|
|
1215
|
+
item.addEventListener("mousedown", (event) => {
|
|
1216
|
+
event.preventDefault();
|
|
1217
|
+
addTag(tag);
|
|
1218
|
+
});
|
|
1219
|
+
suggest.append(item);
|
|
1220
|
+
}
|
|
1221
|
+
suggest.hidden = matches.length === 0;
|
|
1222
|
+
};
|
|
1223
|
+
input.addEventListener("focus", () => {
|
|
1224
|
+
ensureTags().then(showSuggest);
|
|
1225
|
+
});
|
|
1226
|
+
input.addEventListener("input", () => {
|
|
1227
|
+
if (knownTags.length === 0) ensureTags().then(showSuggest);
|
|
1228
|
+
else showSuggest();
|
|
1229
|
+
});
|
|
1230
|
+
input.addEventListener("keydown", (event) => {
|
|
1231
|
+
if (event.key === "Enter" || event.key === ",") {
|
|
1232
|
+
event.preventDefault();
|
|
1233
|
+
commitInput();
|
|
1234
|
+
} else if (event.key === "Backspace" && input.value.length === 0 && chips.length > 0) {
|
|
1235
|
+
chips.pop();
|
|
1236
|
+
renderChips();
|
|
1237
|
+
} else if (event.key === "Escape") hideSuggest();
|
|
1238
|
+
});
|
|
1239
|
+
document.addEventListener("click", (event) => {
|
|
1240
|
+
if (!wrap.contains(event.target)) hideSuggest();
|
|
1241
|
+
}, true);
|
|
1242
|
+
return {
|
|
1243
|
+
el: wrap,
|
|
1244
|
+
getTags: () => {
|
|
1245
|
+
commitInput();
|
|
1246
|
+
return [...chips];
|
|
1247
|
+
},
|
|
1248
|
+
setDefault: (tag) => {
|
|
1249
|
+
if (chips.length === 0) {
|
|
1250
|
+
const t = tag.trim();
|
|
1251
|
+
if (t.length > 0) {
|
|
1252
|
+
chips.push(t);
|
|
1253
|
+
renderChips();
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
};
|
|
1258
|
+
}
|
|
1259
|
+
/**
|
|
1260
|
+
* Markdown editor: a `<pre>` overlay (highlighted by markdown.ts) rendered
|
|
1261
|
+
* behind a transparent-text `<textarea>` with identical metrics. Scroll is
|
|
1262
|
+
* mirrored; on every input the overlay is re-rendered. The text layer stays
|
|
1263
|
+
* exactly aligned with the highlight layer because the highlighter never
|
|
1264
|
+
* changes the user's characters.
|
|
1265
|
+
*/
|
|
1266
|
+
function buildEditor() {
|
|
1267
|
+
const wrap = document.createElement("div");
|
|
1268
|
+
wrap.className = "dsh-tw-note-editor";
|
|
1269
|
+
const hl = document.createElement("pre");
|
|
1270
|
+
hl.className = "dsh-tw-note-hl";
|
|
1271
|
+
hl.setAttribute("aria-hidden", "true");
|
|
1272
|
+
const textarea = document.createElement("textarea");
|
|
1273
|
+
textarea.className = "dsh-tw-note-text";
|
|
1274
|
+
textarea.placeholder = "写点东西… Markdown 高亮,可 📎/拖入文件\nCtrl+Enter 保存";
|
|
1275
|
+
wrap.append(hl, textarea);
|
|
1276
|
+
const render = () => {
|
|
1277
|
+
hl.innerHTML = `${highlightMarkdown(textarea.value)}\n`;
|
|
1278
|
+
hl.scrollTop = textarea.scrollTop;
|
|
1279
|
+
hl.scrollLeft = textarea.scrollLeft;
|
|
1280
|
+
};
|
|
1281
|
+
const syncScroll = () => {
|
|
1282
|
+
hl.scrollTop = textarea.scrollTop;
|
|
1283
|
+
hl.scrollLeft = textarea.scrollLeft;
|
|
1284
|
+
};
|
|
1285
|
+
textarea.addEventListener("input", render);
|
|
1286
|
+
textarea.addEventListener("scroll", syncScroll);
|
|
1287
|
+
render();
|
|
1288
|
+
return {
|
|
1289
|
+
el: wrap,
|
|
1290
|
+
textarea,
|
|
1291
|
+
render,
|
|
1292
|
+
focus: () => textarea.focus()
|
|
1293
|
+
};
|
|
1294
|
+
}
|
|
1295
|
+
/**
|
|
1296
|
+
* Upload one file to the wiki (raw body, name in ?name=), then insert a
|
|
1297
|
+
* Markdown image/link line at the caret of the given editor.
|
|
1298
|
+
*/
|
|
1299
|
+
async function uploadInto(file, editor) {
|
|
1300
|
+
if (file.size > MAX_UPLOAD_BYTES) {
|
|
1301
|
+
toast(`文件过大(≤ ${Math.round(MAX_UPLOAD_BYTES / 1024 / 1024)}MB)`);
|
|
1302
|
+
return;
|
|
1303
|
+
}
|
|
1304
|
+
try {
|
|
1305
|
+
const res = await fetch(`${UPLOAD_ENDPOINT}?name=${encodeURIComponent(file.name)}`, {
|
|
1306
|
+
method: "POST",
|
|
1307
|
+
headers: { "content-type": file.type || "application/octet-stream" },
|
|
1308
|
+
body: file,
|
|
1309
|
+
signal: AbortSignal.timeout(12e4)
|
|
1310
|
+
});
|
|
1311
|
+
const payload = await res.json().catch(() => null);
|
|
1312
|
+
if (!res.ok || payload?.ok !== true) {
|
|
1313
|
+
toast(`上传失败:${payload?.error ?? `HTTP ${res.status}`}`);
|
|
1314
|
+
return;
|
|
1315
|
+
}
|
|
1316
|
+
const name = payload.name ?? file.name;
|
|
1317
|
+
const markdown = file.type.startsWith("image/") ? `` : `[${name}](${payload.url})`;
|
|
1318
|
+
const ta = editor.textarea;
|
|
1319
|
+
const start = ta.selectionStart ?? ta.value.length;
|
|
1320
|
+
const end = ta.selectionEnd ?? start;
|
|
1321
|
+
const before = ta.value.slice(0, start);
|
|
1322
|
+
const after = ta.value.slice(end);
|
|
1323
|
+
const insert = `${start > 0 && !before.endsWith("\n") ? "\n" : ""}${markdown}\n`;
|
|
1324
|
+
ta.value = `${before}${insert}${after}`;
|
|
1325
|
+
const caret = (before + insert).length;
|
|
1326
|
+
ta.setSelectionRange(caret, caret);
|
|
1327
|
+
ta.focus();
|
|
1328
|
+
editor.render();
|
|
1329
|
+
toast(`已上传「${name}」并插入链接`);
|
|
1330
|
+
} catch (err) {
|
|
1331
|
+
toast(`上传失败:${err instanceof Error ? err.message : String(err)}`);
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
/** Build the whole widget DOM and wire it up (returns the appended root). */
|
|
1335
|
+
function buildWidget() {
|
|
1336
|
+
const root = document.createElement("div");
|
|
1337
|
+
root.className = "dsh-tw-note";
|
|
1338
|
+
const card = document.createElement("div");
|
|
1339
|
+
card.className = "dsh-tw-note-card";
|
|
1340
|
+
card.hidden = true;
|
|
1341
|
+
const head = document.createElement("div");
|
|
1342
|
+
head.className = "dsh-tw-note-head";
|
|
1343
|
+
const label = document.createElement("span");
|
|
1344
|
+
label.className = "dsh-tw-note-label";
|
|
1345
|
+
label.textContent = "📝 快速笔记";
|
|
1346
|
+
const closeBtn = document.createElement("button");
|
|
1347
|
+
closeBtn.type = "button";
|
|
1348
|
+
closeBtn.className = "dsh-tw-note-close";
|
|
1349
|
+
closeBtn.title = "收起";
|
|
1350
|
+
closeBtn.textContent = "✕";
|
|
1351
|
+
head.append(label, closeBtn);
|
|
1352
|
+
const fields = document.createElement("div");
|
|
1353
|
+
fields.className = "dsh-tw-note-fields";
|
|
1354
|
+
const titleInput = document.createElement("input");
|
|
1355
|
+
titleInput.className = "dsh-tw-note-title";
|
|
1356
|
+
titleInput.placeholder = "标题(默认时间戳)";
|
|
1357
|
+
const tagEditor = buildTagEditor();
|
|
1358
|
+
fields.append(titleInput, tagEditor.el);
|
|
1359
|
+
const editor = buildEditor();
|
|
1360
|
+
const textarea = editor.textarea;
|
|
1361
|
+
const uploadBtn = document.createElement("button");
|
|
1362
|
+
uploadBtn.type = "button";
|
|
1363
|
+
uploadBtn.className = "dsh-tw-note-upload";
|
|
1364
|
+
uploadBtn.title = "上传文件到 wiki 并插入 Markdown 链接(也可直接拖入编辑器)";
|
|
1365
|
+
uploadBtn.textContent = "📎 上传";
|
|
1366
|
+
const fileInput = document.createElement("input");
|
|
1367
|
+
fileInput.type = "file";
|
|
1368
|
+
fileInput.multiple = true;
|
|
1369
|
+
fileInput.hidden = true;
|
|
1370
|
+
uploadBtn.addEventListener("click", () => {
|
|
1371
|
+
fileInput.click();
|
|
1372
|
+
});
|
|
1373
|
+
fileInput.addEventListener("change", () => {
|
|
1374
|
+
for (const file of Array.from(fileInput.files ?? [])) uploadInto(file, editor);
|
|
1375
|
+
fileInput.value = "";
|
|
1376
|
+
});
|
|
1377
|
+
let dragDepth = 0;
|
|
1378
|
+
editor.el.addEventListener("dragenter", (event) => {
|
|
1379
|
+
event.preventDefault();
|
|
1380
|
+
dragDepth++;
|
|
1381
|
+
editor.el.classList.add("dsh-tw-note-drop");
|
|
1382
|
+
});
|
|
1383
|
+
editor.el.addEventListener("dragover", (event) => {
|
|
1384
|
+
event.preventDefault();
|
|
1385
|
+
});
|
|
1386
|
+
editor.el.addEventListener("dragleave", (event) => {
|
|
1387
|
+
event.preventDefault();
|
|
1388
|
+
dragDepth = Math.max(0, dragDepth - 1);
|
|
1389
|
+
if (dragDepth === 0) editor.el.classList.remove("dsh-tw-note-drop");
|
|
1390
|
+
});
|
|
1391
|
+
editor.el.addEventListener("drop", (event) => {
|
|
1392
|
+
event.preventDefault();
|
|
1393
|
+
dragDepth = 0;
|
|
1394
|
+
editor.el.classList.remove("dsh-tw-note-drop");
|
|
1395
|
+
const files = event.dataTransfer?.files;
|
|
1396
|
+
if (files === void 0 || files.length === 0) return;
|
|
1397
|
+
for (const file of Array.from(files)) uploadInto(file, editor);
|
|
1398
|
+
});
|
|
1399
|
+
const foot = document.createElement("div");
|
|
1400
|
+
foot.className = "dsh-tw-note-foot";
|
|
1401
|
+
const footLeft = document.createElement("div");
|
|
1402
|
+
footLeft.className = "dsh-tw-note-foot-left";
|
|
1403
|
+
const hint = document.createElement("span");
|
|
1404
|
+
hint.className = "dsh-tw-note-hint";
|
|
1405
|
+
hint.textContent = "Ctrl+Enter";
|
|
1406
|
+
footLeft.append(uploadBtn, hint);
|
|
1407
|
+
const footRight = document.createElement("div");
|
|
1408
|
+
footRight.className = "dsh-tw-note-foot-right";
|
|
1409
|
+
const edit = document.createElement("button");
|
|
1410
|
+
edit.type = "button";
|
|
1411
|
+
edit.className = "dsh-tw-note-edit";
|
|
1412
|
+
edit.title = "保存并在 TiddlyWiki 原生编辑器中打开";
|
|
1413
|
+
edit.textContent = "✏️ 在 TW 中编辑";
|
|
1414
|
+
const save = document.createElement("button");
|
|
1415
|
+
save.type = "button";
|
|
1416
|
+
save.className = "dsh-tw-note-save";
|
|
1417
|
+
save.textContent = "保存";
|
|
1418
|
+
footRight.append(edit, save);
|
|
1419
|
+
foot.append(footLeft, footRight);
|
|
1420
|
+
card.append(head, fields, editor.el, foot);
|
|
1421
|
+
const toggle = document.createElement("button");
|
|
1422
|
+
toggle.type = "button";
|
|
1423
|
+
toggle.className = "dsh-tw-note-toggle";
|
|
1424
|
+
toggle.textContent = "📝 快速笔记";
|
|
1425
|
+
root.append(card, toggle);
|
|
1426
|
+
document.body.append(root);
|
|
1427
|
+
let defaultTag = "inbox";
|
|
1428
|
+
let opened = false;
|
|
1429
|
+
const resetTitle = () => {
|
|
1430
|
+
titleInput.value = timestampTitle();
|
|
1431
|
+
};
|
|
1432
|
+
const open = async () => {
|
|
1433
|
+
card.hidden = false;
|
|
1434
|
+
opened = true;
|
|
1435
|
+
resetTitle();
|
|
1436
|
+
editor.focus();
|
|
1437
|
+
defaultTag = (await fetchUiOptions()).defaultTag;
|
|
1438
|
+
tagEditor.setDefault(defaultTag);
|
|
1439
|
+
};
|
|
1440
|
+
const close = () => {
|
|
1441
|
+
card.hidden = true;
|
|
1442
|
+
opened = false;
|
|
1443
|
+
};
|
|
1444
|
+
toggle.addEventListener("click", () => {
|
|
1445
|
+
opened ? close() : open();
|
|
1446
|
+
});
|
|
1447
|
+
closeBtn.addEventListener("click", close);
|
|
1448
|
+
const doSave = async () => {
|
|
1449
|
+
const text = textarea.value.trim();
|
|
1450
|
+
if (text.length === 0) {
|
|
1451
|
+
toast("内容为空,未保存");
|
|
1452
|
+
return;
|
|
1453
|
+
}
|
|
1454
|
+
save.disabled = true;
|
|
1455
|
+
save.textContent = "保存中…";
|
|
1456
|
+
try {
|
|
1457
|
+
const res = await fetch(NOTE_ENDPOINT, {
|
|
1458
|
+
method: "POST",
|
|
1459
|
+
headers: { "content-type": "application/json" },
|
|
1460
|
+
body: JSON.stringify({
|
|
1461
|
+
title: titleInput.value.trim(),
|
|
1462
|
+
tags: tagEditor.getTags(),
|
|
1463
|
+
text
|
|
1464
|
+
}),
|
|
1465
|
+
signal: AbortSignal.timeout(1e4)
|
|
1466
|
+
});
|
|
1467
|
+
const payload = await res.json().catch(() => null);
|
|
1468
|
+
if (!res.ok || payload?.ok !== true) {
|
|
1469
|
+
toast(`保存失败:${payload?.error ?? `HTTP ${res.status}`}`);
|
|
1470
|
+
return;
|
|
1471
|
+
}
|
|
1472
|
+
textarea.value = "";
|
|
1473
|
+
editor.render();
|
|
1474
|
+
resetTitle();
|
|
1475
|
+
toast(`已保存「${payload.title ?? titleInput.value}」`);
|
|
1476
|
+
close();
|
|
1477
|
+
} catch (err) {
|
|
1478
|
+
toast(`保存失败:${err instanceof Error ? err.message : String(err)}`);
|
|
1479
|
+
} finally {
|
|
1480
|
+
save.disabled = false;
|
|
1481
|
+
save.textContent = "保存";
|
|
1482
|
+
}
|
|
1483
|
+
};
|
|
1484
|
+
save.addEventListener("click", () => {
|
|
1485
|
+
doSave();
|
|
1486
|
+
});
|
|
1487
|
+
textarea.addEventListener("keydown", (event) => {
|
|
1488
|
+
if ((event.ctrlKey || event.metaKey) && event.key === "Enter") {
|
|
1489
|
+
event.preventDefault();
|
|
1490
|
+
doSave();
|
|
1491
|
+
}
|
|
1492
|
+
});
|
|
1493
|
+
/** Save (if non-empty) and open the tiddler in TW's native editor. */
|
|
1494
|
+
const doEdit = async () => {
|
|
1495
|
+
const title = titleInput.value.trim().length > 0 ? titleInput.value.trim() : timestampTitle();
|
|
1496
|
+
const text = textarea.value;
|
|
1497
|
+
const tags = tagEditor.getTags();
|
|
1498
|
+
edit.disabled = true;
|
|
1499
|
+
edit.textContent = "打开中…";
|
|
1500
|
+
try {
|
|
1501
|
+
const res = await fetch(EDIT_ENDPOINT, {
|
|
1502
|
+
method: "POST",
|
|
1503
|
+
headers: { "content-type": "application/json" },
|
|
1504
|
+
body: JSON.stringify({
|
|
1505
|
+
title,
|
|
1506
|
+
tags,
|
|
1507
|
+
text
|
|
1508
|
+
}),
|
|
1509
|
+
signal: AbortSignal.timeout(1e4)
|
|
1510
|
+
});
|
|
1511
|
+
const payload = await res.json().catch(() => null);
|
|
1512
|
+
if (!res.ok || payload?.ok !== true) {
|
|
1513
|
+
toast(`打开失败:${payload?.error ?? `HTTP ${res.status}`}`);
|
|
1514
|
+
return;
|
|
1515
|
+
}
|
|
1516
|
+
if (typeof payload.twUrl !== "string" || typeof payload.draftTitle !== "string") {
|
|
1517
|
+
toast("打开失败:服务未返回编辑器地址");
|
|
1518
|
+
return;
|
|
1519
|
+
}
|
|
1520
|
+
openEditorPopup(`${payload.twUrl}#${encodeURIComponent(payload.draftTitle)}`, payload.title ?? title);
|
|
1521
|
+
toast(`已在弹出窗口打开「${payload.title ?? title}」编辑器`);
|
|
1522
|
+
} catch (err) {
|
|
1523
|
+
toast(`打开失败:${err instanceof Error ? err.message : String(err)}`);
|
|
1524
|
+
} finally {
|
|
1525
|
+
edit.disabled = false;
|
|
1526
|
+
edit.textContent = "✏️ 在 TW 中编辑";
|
|
1527
|
+
}
|
|
1528
|
+
};
|
|
1529
|
+
edit.addEventListener("click", () => {
|
|
1530
|
+
doEdit();
|
|
1531
|
+
});
|
|
1532
|
+
return root;
|
|
1533
|
+
}
|
|
1534
|
+
/**
|
|
1535
|
+
* Mount the floating quick-note widget. Fetches /status first: when
|
|
1536
|
+
* `ui.showQuickNote` is off the widget is never created (no DOM side effects).
|
|
1537
|
+
* Returns a disposer that removes it.
|
|
1538
|
+
*/
|
|
1539
|
+
function mountNoteWidget() {
|
|
1540
|
+
let disposed = false;
|
|
1541
|
+
let root;
|
|
1542
|
+
const disposer = () => {
|
|
1543
|
+
disposed = true;
|
|
1544
|
+
root?.remove();
|
|
1545
|
+
document.querySelector(".dsh-tw-toast")?.remove();
|
|
1546
|
+
};
|
|
1547
|
+
(async () => {
|
|
1548
|
+
const ui = await fetchUiOptions();
|
|
1549
|
+
if (disposed || !ui.showQuickNote) return;
|
|
1550
|
+
root = buildWidget();
|
|
1551
|
+
})();
|
|
1552
|
+
return disposer;
|
|
1553
|
+
}
|
|
1554
|
+
//#endregion
|
|
1555
|
+
//#region src/client/sync-button.ts
|
|
1556
|
+
/**
|
|
1557
|
+
* Floating "sync" button (bottom-right) — one click pulls + commits + pushes
|
|
1558
|
+
* the wiki git repository and reports the result, with a live status dot so
|
|
1559
|
+
* the human always sees whether the knowledge base is in sync.
|
|
1560
|
+
*
|
|
1561
|
+
* - honors `ui.showSyncButton`: the button is mounted async and never created
|
|
1562
|
+
* when the option is off (settings page toggle);
|
|
1563
|
+
* - polls /dsh-tiddlywiki/status every 30s for the git state;
|
|
1564
|
+
* - clicking POSTs /dsh-tiddlywiki/sync (pull → commit → push) and toasts the
|
|
1565
|
+
* outcome, then refreshes the dot immediately.
|
|
1566
|
+
*
|
|
1567
|
+
* Status dot mapping (git summary from /status):
|
|
1568
|
+
* offline → gray (no repo / service unreachable)
|
|
1569
|
+
* dirty → amber (uncommitted changes)
|
|
1570
|
+
* behind → red (remote has commits we don't — pull will rebase)
|
|
1571
|
+
* clean → green (worktree clean)
|
|
1572
|
+
*
|
|
1573
|
+
* @module dsh-tiddlywiki/client/sync-button
|
|
1574
|
+
*/
|
|
1575
|
+
const STATUS_ENDPOINT = "/dsh-tiddlywiki/status";
|
|
1576
|
+
const SYNC_ENDPOINT$1 = "/dsh-tiddlywiki/sync";
|
|
1577
|
+
const POLL_MS = 3e4;
|
|
1578
|
+
/** Inline sync icon (circular arrows, stroke-based, theme-colored). */
|
|
1579
|
+
const SYNC_ICON = "<svg viewBox=\"0 0 16 16\" width=\"15\" height=\"15\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.6\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"M13 8a5 5 0 1 1-1.4-3.5\"/><path d=\"M13 2.5v3h-3\"/></svg>";
|
|
1580
|
+
function pad(n) {
|
|
1581
|
+
return n < 10 ? `0${n}` : String(n);
|
|
1582
|
+
}
|
|
1583
|
+
/** Compact local time for the tooltip (e.g. "10:32"). */
|
|
1584
|
+
function clock(date) {
|
|
1585
|
+
return `${pad(date.getHours())}:${pad(date.getMinutes())}`;
|
|
1586
|
+
}
|
|
1587
|
+
async function fetchStatus() {
|
|
1588
|
+
try {
|
|
1589
|
+
const res = await fetch(STATUS_ENDPOINT, { signal: AbortSignal.timeout(8e3) });
|
|
1590
|
+
if (!res.ok) return null;
|
|
1591
|
+
return await res.json();
|
|
1592
|
+
} catch {
|
|
1593
|
+
return null;
|
|
1594
|
+
}
|
|
1595
|
+
}
|
|
1596
|
+
/**
|
|
1597
|
+
* Mount the floating sync button. Fetches /status first: when
|
|
1598
|
+
* `ui.showSyncButton` is off the button is never created. Returns a disposer.
|
|
1599
|
+
*/
|
|
1600
|
+
function mountSyncButton() {
|
|
1601
|
+
let disposed = false;
|
|
1602
|
+
let root;
|
|
1603
|
+
let dot;
|
|
1604
|
+
let label;
|
|
1605
|
+
let lastSync;
|
|
1606
|
+
let timer;
|
|
1607
|
+
const build = () => {
|
|
1608
|
+
const btn = document.createElement("button");
|
|
1609
|
+
btn.type = "button";
|
|
1610
|
+
btn.className = "dsh-tw-sync";
|
|
1611
|
+
btn.title = "同步知识库(pull → commit → push)";
|
|
1612
|
+
const icon = document.createElement("span");
|
|
1613
|
+
icon.className = "dsh-tw-sync-icon";
|
|
1614
|
+
icon.innerHTML = SYNC_ICON;
|
|
1615
|
+
dot = document.createElement("span");
|
|
1616
|
+
dot.className = "dsh-tw-sync-dot";
|
|
1617
|
+
dot.dataset.state = "offline";
|
|
1618
|
+
label = document.createElement("span");
|
|
1619
|
+
label.className = "dsh-tw-sync-label";
|
|
1620
|
+
label.textContent = "同步";
|
|
1621
|
+
btn.append(icon, label, dot);
|
|
1622
|
+
btn.addEventListener("click", () => {
|
|
1623
|
+
doSync();
|
|
1624
|
+
});
|
|
1625
|
+
document.body.append(btn);
|
|
1626
|
+
return btn;
|
|
1627
|
+
};
|
|
1628
|
+
/** Map a git summary onto the dot + label + tooltip. */
|
|
1629
|
+
const applyStatus = (payload) => {
|
|
1630
|
+
if (root === void 0 || dot === void 0 || label === void 0) return;
|
|
1631
|
+
const git = payload?.git;
|
|
1632
|
+
const bits = ["同步知识库"];
|
|
1633
|
+
let state = "offline";
|
|
1634
|
+
let word = "离线";
|
|
1635
|
+
if (git === null || git === void 0 || git.exists !== true) bits.push("git 仓库不可用");
|
|
1636
|
+
else {
|
|
1637
|
+
bits.push(`分支 ${git.branch ?? "?"}`);
|
|
1638
|
+
if (typeof git.lastCommit === "string") bits.push(git.lastCommit);
|
|
1639
|
+
const behind = typeof git.behind === "number" ? git.behind : 0;
|
|
1640
|
+
const ahead = typeof git.ahead === "number" ? git.ahead : 0;
|
|
1641
|
+
if (ahead > 0) bits.push(`领先 ${ahead}`);
|
|
1642
|
+
if (behind > 0) bits.push(`落后 ${behind}`);
|
|
1643
|
+
if (git.dirty === true) {
|
|
1644
|
+
state = "dirty";
|
|
1645
|
+
word = "待提交";
|
|
1646
|
+
bits.push(`有 ${git.dirtyFiles?.length ?? 0} 个未提交改动`);
|
|
1647
|
+
} else if (behind > 0) {
|
|
1648
|
+
state = "behind";
|
|
1649
|
+
word = "可更新";
|
|
1650
|
+
} else {
|
|
1651
|
+
state = "clean";
|
|
1652
|
+
word = "已同步";
|
|
1653
|
+
}
|
|
1654
|
+
}
|
|
1655
|
+
if (lastSync !== void 0) bits.push(`上次同步 ${clock(lastSync)}`);
|
|
1656
|
+
root.title = bits.join(" · ");
|
|
1657
|
+
dot.dataset.state = state;
|
|
1658
|
+
label.textContent = word;
|
|
1659
|
+
};
|
|
1660
|
+
const doSync = async () => {
|
|
1661
|
+
if (root === void 0) return;
|
|
1662
|
+
root.disabled = true;
|
|
1663
|
+
root.classList.add("dsh-tw-sync-spin");
|
|
1664
|
+
if (label !== void 0) label.textContent = "同步中…";
|
|
1665
|
+
if (dot !== void 0) dot.dataset.state = "syncing";
|
|
1666
|
+
try {
|
|
1667
|
+
const res = await fetch(SYNC_ENDPOINT$1, {
|
|
1668
|
+
method: "POST",
|
|
1669
|
+
signal: AbortSignal.timeout(12e4)
|
|
1670
|
+
});
|
|
1671
|
+
const payload = await res.json().catch(() => null);
|
|
1672
|
+
lastSync = /* @__PURE__ */ new Date();
|
|
1673
|
+
if (payload === null || payload.ok !== true) toast(`同步失败:${payload?.error ?? payload?.message ?? `HTTP ${res.status}`}`);
|
|
1674
|
+
else {
|
|
1675
|
+
let detail = "";
|
|
1676
|
+
if (payload.push && payload.push !== "nothing to commit") detail = `(${payload.push})`;
|
|
1677
|
+
if (payload.changed === true) {
|
|
1678
|
+
detail += payload.restarted === true ? ",TW 已重启" : ",TW 未自动重启";
|
|
1679
|
+
if (payload.restartError) detail += `(${payload.restartError})`;
|
|
1680
|
+
}
|
|
1681
|
+
toast(`同步完成:${payload.message ?? "OK"}${detail}`);
|
|
1682
|
+
}
|
|
1683
|
+
applyStatus(await fetchStatus());
|
|
1684
|
+
} catch (err) {
|
|
1685
|
+
toast(`同步失败:${err instanceof Error ? err.message : String(err)}`);
|
|
1686
|
+
} finally {
|
|
1687
|
+
root.disabled = false;
|
|
1688
|
+
root.classList.remove("dsh-tw-sync-spin");
|
|
1689
|
+
}
|
|
1690
|
+
};
|
|
1691
|
+
const poll = async () => {
|
|
1692
|
+
applyStatus(await fetchStatus());
|
|
1693
|
+
};
|
|
1694
|
+
(async () => {
|
|
1695
|
+
const payload = await fetchStatus();
|
|
1696
|
+
if (disposed || payload?.ui?.showSyncButton === false) return;
|
|
1697
|
+
root = build();
|
|
1698
|
+
applyStatus(payload);
|
|
1699
|
+
timer = window.setInterval(() => {
|
|
1700
|
+
poll();
|
|
1701
|
+
}, POLL_MS);
|
|
1702
|
+
})();
|
|
1703
|
+
return () => {
|
|
1704
|
+
disposed = true;
|
|
1705
|
+
if (timer !== void 0) window.clearInterval(timer);
|
|
1706
|
+
root?.remove();
|
|
1707
|
+
};
|
|
1708
|
+
}
|
|
1709
|
+
//#endregion
|
|
1710
|
+
//#region src/client/settings-page.ts
|
|
1711
|
+
/**
|
|
1712
|
+
* Settings-page half (design doc §13, config panel): a pure-DOM page mounted
|
|
1713
|
+
* inside a `settings.section` React wrapper. Everything talks to the host
|
|
1714
|
+
* admin routes — same-origin JSON, no client services beyond `slots`:
|
|
1715
|
+
*
|
|
1716
|
+
* GET /dsh-tiddlywiki/admin/state current info + catalog + config
|
|
1717
|
+
* POST /dsh-tiddlywiki/admin/info { plugins?, themes? } → restart TW
|
|
1718
|
+
* POST /dsh-tiddlywiki/admin/config { ...patch } → persist
|
|
1719
|
+
* POST /dsh-tiddlywiki/admin/restart restart the TW child
|
|
1720
|
+
*
|
|
1721
|
+
* Sections:
|
|
1722
|
+
* 1. 状态/重启 TW 运行状态 + git 概览 + 重启按钮
|
|
1723
|
+
* 2. 常规配置 note.tag / git.* / uiLanguage(改了什么保存什么)
|
|
1724
|
+
* 3. 插件管理 自带官方插件勾选(可搜索)→ 应用并重启 TW
|
|
1725
|
+
* 4. 主题管理 自带主题单选 → 应用并重启 TW
|
|
1726
|
+
*
|
|
1727
|
+
* @module dsh-tiddlywiki/client/settings-page
|
|
1728
|
+
*/
|
|
1729
|
+
const STATE_ENDPOINT = "/dsh-tiddlywiki/admin/state";
|
|
1730
|
+
const INFO_ENDPOINT = "/dsh-tiddlywiki/admin/info";
|
|
1731
|
+
const CONFIG_ENDPOINT = "/dsh-tiddlywiki/admin/config";
|
|
1732
|
+
const RESTART_ENDPOINT = "/dsh-tiddlywiki/admin/restart";
|
|
1733
|
+
const SYNC_ENDPOINT = "/dsh-tiddlywiki/sync";
|
|
1734
|
+
function make(tag, className, text) {
|
|
1735
|
+
const node = document.createElement(tag);
|
|
1736
|
+
if (className !== void 0) node.className = className;
|
|
1737
|
+
if (text !== void 0) node.textContent = text;
|
|
1738
|
+
return node;
|
|
1739
|
+
}
|
|
1740
|
+
async function fetchJson(url, init) {
|
|
1741
|
+
const res = await fetch(url, {
|
|
1742
|
+
...init,
|
|
1743
|
+
signal: AbortSignal.timeout(15e3)
|
|
1744
|
+
});
|
|
1745
|
+
const data = await res.json().catch(() => ({}));
|
|
1746
|
+
if (!res.ok) {
|
|
1747
|
+
const err = data.error ?? `HTTP ${res.status}`;
|
|
1748
|
+
throw new Error(err);
|
|
1749
|
+
}
|
|
1750
|
+
return data;
|
|
1751
|
+
}
|
|
1752
|
+
function mountSettingsPage(container) {
|
|
1753
|
+
let disposed = false;
|
|
1754
|
+
container.classList.add("dsh-tw-settings");
|
|
1755
|
+
const statusRow = make("div", "dsh-tw-settings-row dsh-tw-settings-status");
|
|
1756
|
+
const body = make("div", "dsh-tw-settings-body");
|
|
1757
|
+
container.append(statusRow, body);
|
|
1758
|
+
const disposers = [];
|
|
1759
|
+
const refresh = async () => {
|
|
1760
|
+
try {
|
|
1761
|
+
const state = await fetchJson(STATE_ENDPOINT);
|
|
1762
|
+
if (disposed) return;
|
|
1763
|
+
renderStatus(statusRow, state, refresh);
|
|
1764
|
+
renderMain(body, state, refresh);
|
|
1765
|
+
} catch (err) {
|
|
1766
|
+
if (disposed) return;
|
|
1767
|
+
body.replaceChildren();
|
|
1768
|
+
statusRow.replaceChildren();
|
|
1769
|
+
const msg = make("div", "dsh-tw-settings-error", `加载配置失败:${err instanceof Error ? err.message : String(err)}`);
|
|
1770
|
+
const retry = make("button", "dsh-tw-settings-btn", "重试");
|
|
1771
|
+
retry.type = "button";
|
|
1772
|
+
retry.addEventListener("click", () => {
|
|
1773
|
+
refresh();
|
|
1774
|
+
});
|
|
1775
|
+
body.append(msg, retry);
|
|
1776
|
+
}
|
|
1777
|
+
};
|
|
1778
|
+
refresh();
|
|
1779
|
+
disposers.push(() => {
|
|
1780
|
+
disposed = true;
|
|
1781
|
+
container.replaceChildren();
|
|
1782
|
+
container.classList.remove("dsh-tw-settings");
|
|
1783
|
+
});
|
|
1784
|
+
return () => {
|
|
1785
|
+
for (const dispose of disposers.splice(0)) dispose();
|
|
1786
|
+
};
|
|
1787
|
+
}
|
|
1788
|
+
function renderStatus(row, state, refresh) {
|
|
1789
|
+
row.replaceChildren();
|
|
1790
|
+
const server = state.server ?? {};
|
|
1791
|
+
const status = server.status ?? "unknown";
|
|
1792
|
+
const chip = make("span", `dsh-tw-settings-chip`, status);
|
|
1793
|
+
chip.dataset.state = status;
|
|
1794
|
+
const label = make("span", "dsh-tw-settings-muted", [
|
|
1795
|
+
server.url !== void 0 ? `TW ${server.url}` : "",
|
|
1796
|
+
state.git?.branch !== void 0 ? `git ${state.git.branch}` : "",
|
|
1797
|
+
state.git?.lastCommit !== void 0 ? state.git.lastCommit : "",
|
|
1798
|
+
state.git?.dirty === true ? "有未提交改动" : ""
|
|
1799
|
+
].filter(Boolean).join(" · "));
|
|
1800
|
+
const sync = make("button", "dsh-tw-settings-btn", "同步");
|
|
1801
|
+
sync.type = "button";
|
|
1802
|
+
sync.title = "git 同步(pull → commit → push)";
|
|
1803
|
+
sync.addEventListener("click", () => {
|
|
1804
|
+
sync.disabled = true;
|
|
1805
|
+
sync.textContent = "同步中…";
|
|
1806
|
+
(async () => {
|
|
1807
|
+
try {
|
|
1808
|
+
const res = await fetch(SYNC_ENDPOINT, {
|
|
1809
|
+
method: "POST",
|
|
1810
|
+
signal: AbortSignal.timeout(12e4)
|
|
1811
|
+
});
|
|
1812
|
+
const payload = await res.json().catch(() => null);
|
|
1813
|
+
if (!res.ok || payload?.ok !== true) toast(`同步失败:${payload?.error ?? payload?.message ?? `HTTP ${res.status}`}`);
|
|
1814
|
+
else {
|
|
1815
|
+
let detail = "";
|
|
1816
|
+
if (payload.changed === true) {
|
|
1817
|
+
detail += payload.restarted === true ? ",TW 已重启" : ",TW 未自动重启";
|
|
1818
|
+
if (payload.restartError) detail += `(${payload.restartError})`;
|
|
1819
|
+
}
|
|
1820
|
+
toast(`同步完成:${payload.message ?? "OK"}${detail}`);
|
|
1821
|
+
}
|
|
1822
|
+
} catch (err) {
|
|
1823
|
+
toast(`同步失败:${err instanceof Error ? err.message : String(err)}`);
|
|
1824
|
+
} finally {
|
|
1825
|
+
sync.disabled = false;
|
|
1826
|
+
sync.textContent = "同步";
|
|
1827
|
+
refresh();
|
|
1828
|
+
}
|
|
1829
|
+
})();
|
|
1830
|
+
});
|
|
1831
|
+
const restart = make("button", "dsh-tw-settings-btn", "重启 TW");
|
|
1832
|
+
restart.type = "button";
|
|
1833
|
+
restart.addEventListener("click", () => {
|
|
1834
|
+
restart.disabled = true;
|
|
1835
|
+
restart.textContent = "重启中…";
|
|
1836
|
+
(async () => {
|
|
1837
|
+
try {
|
|
1838
|
+
await fetchJson(RESTART_ENDPOINT, { method: "POST" });
|
|
1839
|
+
toast("TW 已重启");
|
|
1840
|
+
} catch (err) {
|
|
1841
|
+
toast(`重启失败:${err instanceof Error ? err.message : String(err)}`);
|
|
1842
|
+
} finally {
|
|
1843
|
+
restart.disabled = false;
|
|
1844
|
+
restart.textContent = "重启 TW";
|
|
1845
|
+
refresh();
|
|
1846
|
+
}
|
|
1847
|
+
})();
|
|
1848
|
+
});
|
|
1849
|
+
row.append(chip, label, sync, restart);
|
|
1850
|
+
}
|
|
1851
|
+
/** Config section: fields bound to effective config, changed-only save. */
|
|
1852
|
+
function renderConfigSection(body, config, refresh) {
|
|
1853
|
+
const section = make("section", "dsh-tw-settings-section");
|
|
1854
|
+
section.append(make("h3", "dsh-tw-settings-h", "常规配置"));
|
|
1855
|
+
const note = config.note ?? {};
|
|
1856
|
+
const git = config.git ?? {};
|
|
1857
|
+
const ui = config.ui ?? {};
|
|
1858
|
+
const fields = [];
|
|
1859
|
+
const textField = (key, label, initial) => {
|
|
1860
|
+
const input = make("input", "dsh-tw-settings-input");
|
|
1861
|
+
input.value = initial;
|
|
1862
|
+
const wrap = make("label", "dsh-tw-settings-field");
|
|
1863
|
+
wrap.append(make("span", "dsh-tw-settings-label", label), input);
|
|
1864
|
+
fields.push({
|
|
1865
|
+
key,
|
|
1866
|
+
input,
|
|
1867
|
+
initial,
|
|
1868
|
+
read: () => input.value.trim(),
|
|
1869
|
+
changed: () => input.value.trim() !== initial
|
|
1870
|
+
});
|
|
1871
|
+
section.append(wrap);
|
|
1872
|
+
};
|
|
1873
|
+
const checkField = (key, label, initial) => {
|
|
1874
|
+
const input = make("input", "dsh-tw-settings-check");
|
|
1875
|
+
input.type = "checkbox";
|
|
1876
|
+
input.checked = initial;
|
|
1877
|
+
const wrap = make("label", "dsh-tw-settings-field dsh-tw-settings-field-check");
|
|
1878
|
+
wrap.append(input, make("span", "dsh-tw-settings-label", label));
|
|
1879
|
+
fields.push({
|
|
1880
|
+
key,
|
|
1881
|
+
input,
|
|
1882
|
+
initial,
|
|
1883
|
+
read: () => input.checked,
|
|
1884
|
+
changed: () => input.checked !== initial
|
|
1885
|
+
});
|
|
1886
|
+
section.append(wrap);
|
|
1887
|
+
};
|
|
1888
|
+
const numField = (key, label, initial) => {
|
|
1889
|
+
const input = make("input", "dsh-tw-settings-input");
|
|
1890
|
+
input.type = "number";
|
|
1891
|
+
input.value = String(initial);
|
|
1892
|
+
const wrap = make("label", "dsh-tw-settings-field");
|
|
1893
|
+
wrap.append(make("span", "dsh-tw-settings-label", label), input);
|
|
1894
|
+
fields.push({
|
|
1895
|
+
key,
|
|
1896
|
+
input,
|
|
1897
|
+
initial,
|
|
1898
|
+
read: () => Number(input.value) || initial,
|
|
1899
|
+
changed: () => (Number(input.value) || initial) !== initial
|
|
1900
|
+
});
|
|
1901
|
+
section.append(wrap);
|
|
1902
|
+
};
|
|
1903
|
+
textField("note.tag", "快速笔记默认 tag", typeof note.tag === "string" ? note.tag : "inbox");
|
|
1904
|
+
checkField("git.autoCommit", "自动 commit(防抖)", git.autoCommit !== false);
|
|
1905
|
+
numField("git.debounceMs", "自动 commit 防抖(ms)", typeof git.debounceMs === "number" ? git.debounceMs : 6e4);
|
|
1906
|
+
textField("git.remote", "git 远端(空=仅本地)", typeof git.remote === "string" ? git.remote : "");
|
|
1907
|
+
textField("git.branch", "git 分支", typeof git.branch === "string" ? git.branch : "main");
|
|
1908
|
+
checkField("ui.showQuickNote", "显示「快速笔记」悬浮按钮", ui.showQuickNote !== false);
|
|
1909
|
+
checkField("ui.showPanelStatus", "显示 TW 面板右下角「状态/重载」悬浮按钮", ui.showPanelStatus !== false);
|
|
1910
|
+
checkField("ui.showSyncButton", "显示右下角「同步」悬浮按钮", ui.showSyncButton !== false);
|
|
1911
|
+
const save = make("button", "dsh-tw-settings-btn dsh-tw-settings-primary", "保存配置");
|
|
1912
|
+
save.type = "button";
|
|
1913
|
+
save.addEventListener("click", () => {
|
|
1914
|
+
save.disabled = true;
|
|
1915
|
+
(async () => {
|
|
1916
|
+
const patch = {};
|
|
1917
|
+
for (const field of fields) {
|
|
1918
|
+
if (!field.changed()) continue;
|
|
1919
|
+
const parts = field.key.split(".");
|
|
1920
|
+
if (parts.length === 1) {
|
|
1921
|
+
const key = parts[0];
|
|
1922
|
+
if (key !== void 0) patch[key] = field.read();
|
|
1923
|
+
} else {
|
|
1924
|
+
const [top, rest] = parts;
|
|
1925
|
+
const obj = patch[top] ?? {};
|
|
1926
|
+
obj[rest] = field.read();
|
|
1927
|
+
patch[top] = obj;
|
|
1928
|
+
}
|
|
1929
|
+
}
|
|
1930
|
+
try {
|
|
1931
|
+
await fetchJson(CONFIG_ENDPOINT, {
|
|
1932
|
+
method: "POST",
|
|
1933
|
+
headers: { "content-type": "application/json" },
|
|
1934
|
+
body: JSON.stringify(patch)
|
|
1935
|
+
});
|
|
1936
|
+
toast("配置已保存");
|
|
1937
|
+
refresh();
|
|
1938
|
+
} catch (err) {
|
|
1939
|
+
toast(`保存失败:${err instanceof Error ? err.message : String(err)}`);
|
|
1940
|
+
} finally {
|
|
1941
|
+
save.disabled = false;
|
|
1942
|
+
}
|
|
1943
|
+
})();
|
|
1944
|
+
});
|
|
1945
|
+
section.append(save);
|
|
1946
|
+
body.append(section);
|
|
1947
|
+
}
|
|
1948
|
+
/** Plugin/theme manager: checkboxes/radios + apply (writes info, restarts). */
|
|
1949
|
+
function renderCatalogSection(body, info, catalog, refresh) {
|
|
1950
|
+
const plugins = catalog?.plugins ?? [];
|
|
1951
|
+
const themes = catalog?.themes ?? [];
|
|
1952
|
+
const languages = catalog?.languages ?? [];
|
|
1953
|
+
const activePlugins = new Set(info?.plugins ?? []);
|
|
1954
|
+
const loadedThemes = new Set(info?.themes ?? []);
|
|
1955
|
+
const activeLanguages = new Set(info?.languages ?? []);
|
|
1956
|
+
const pluginSection = make("section", "dsh-tw-settings-section");
|
|
1957
|
+
pluginSection.append(make("h3", "dsh-tw-settings-h", "插件管理(自带官方插件)"));
|
|
1958
|
+
const search = make("input", "dsh-tw-settings-input dsh-tw-settings-search");
|
|
1959
|
+
search.placeholder = "搜索插件…";
|
|
1960
|
+
const listWrap = make("div", "dsh-tw-settings-list");
|
|
1961
|
+
const applyPlugins = make("button", "dsh-tw-settings-btn dsh-tw-settings-primary", "应用插件(重启 TW)");
|
|
1962
|
+
applyPlugins.type = "button";
|
|
1963
|
+
const renderPluginList = (needle) => {
|
|
1964
|
+
listWrap.replaceChildren();
|
|
1965
|
+
const q = needle.toLowerCase();
|
|
1966
|
+
for (const plugin of plugins) {
|
|
1967
|
+
if (q.length > 0 && !`${plugin.label} ${plugin.name} ${plugin.description}`.toLowerCase().includes(q)) continue;
|
|
1968
|
+
const input = make("input", "dsh-tw-settings-check");
|
|
1969
|
+
input.type = "checkbox";
|
|
1970
|
+
input.checked = activePlugins.has(plugin.name);
|
|
1971
|
+
input.addEventListener("change", () => {
|
|
1972
|
+
if (input.checked) activePlugins.add(plugin.name);
|
|
1973
|
+
else activePlugins.delete(plugin.name);
|
|
1974
|
+
});
|
|
1975
|
+
const label = make("label", "dsh-tw-settings-row dsh-tw-settings-plugin");
|
|
1976
|
+
const name = make("span", "dsh-tw-settings-name", plugin.label);
|
|
1977
|
+
name.title = plugin.name;
|
|
1978
|
+
const desc = make("span", "dsh-tw-settings-muted", plugin.description || plugin.name);
|
|
1979
|
+
label.append(input, name, desc);
|
|
1980
|
+
listWrap.append(label);
|
|
1981
|
+
}
|
|
1982
|
+
};
|
|
1983
|
+
search.addEventListener("input", () => renderPluginList(search.value));
|
|
1984
|
+
applyPlugins.addEventListener("click", () => {
|
|
1985
|
+
applyPlugins.disabled = true;
|
|
1986
|
+
(async () => {
|
|
1987
|
+
try {
|
|
1988
|
+
await fetchJson(INFO_ENDPOINT, {
|
|
1989
|
+
method: "POST",
|
|
1990
|
+
headers: { "content-type": "application/json" },
|
|
1991
|
+
body: JSON.stringify({ plugins: [...activePlugins] })
|
|
1992
|
+
});
|
|
1993
|
+
toast("插件已应用,TW 已重启");
|
|
1994
|
+
refresh();
|
|
1995
|
+
} catch (err) {
|
|
1996
|
+
toast(`应用失败:${err instanceof Error ? err.message : String(err)}`);
|
|
1997
|
+
applyPlugins.disabled = false;
|
|
1998
|
+
}
|
|
1999
|
+
})();
|
|
2000
|
+
});
|
|
2001
|
+
renderPluginList("");
|
|
2002
|
+
pluginSection.append(search, listWrap, applyPlugins);
|
|
2003
|
+
body.append(pluginSection);
|
|
2004
|
+
const themeSection = make("section", "dsh-tw-settings-section");
|
|
2005
|
+
themeSection.append(make("h3", "dsh-tw-settings-h", "主题管理(自带主题)"));
|
|
2006
|
+
const themeHint = make("div", "dsh-tw-settings-muted", "「加载」= TW 里可用的主题(可多选,依赖链自动带上,如 heavier 会带 snowwhite+vanilla);「活动」= 当前视觉主题(单选,自动加入加载集)。应用后重启 TW。");
|
|
2007
|
+
const themeHead = make("div", "dsh-tw-settings-row dsh-tw-settings-head");
|
|
2008
|
+
themeHead.append(make("span", "dsh-tw-settings-col", "加载"), make("span", "dsh-tw-settings-col", "活动"), make("span", "dsh-tw-settings-name", "主题"));
|
|
2009
|
+
const themeList = info?.themes ?? [];
|
|
2010
|
+
let activeThemeName = themeList.length > 0 ? themeList[themeList.length - 1] : "tiddlywiki/vanilla";
|
|
2011
|
+
const themeWrap = make("div", "dsh-tw-settings-list");
|
|
2012
|
+
for (const theme of themes) {
|
|
2013
|
+
const load = make("input", "dsh-tw-settings-check");
|
|
2014
|
+
load.type = "checkbox";
|
|
2015
|
+
load.checked = loadedThemes.has(theme.name);
|
|
2016
|
+
load.title = "加载该主题";
|
|
2017
|
+
load.addEventListener("change", () => {
|
|
2018
|
+
if (load.checked) loadedThemes.add(theme.name);
|
|
2019
|
+
else loadedThemes.delete(theme.name);
|
|
2020
|
+
});
|
|
2021
|
+
const act = make("input", "dsh-tw-settings-check");
|
|
2022
|
+
act.type = "radio";
|
|
2023
|
+
act.name = "dsh-tw-active-theme";
|
|
2024
|
+
act.checked = theme.name === activeThemeName;
|
|
2025
|
+
act.title = "设为活动主题";
|
|
2026
|
+
act.addEventListener("change", () => {
|
|
2027
|
+
if (act.checked) activeThemeName = theme.name;
|
|
2028
|
+
});
|
|
2029
|
+
const name = make("span", "dsh-tw-settings-name", theme.label);
|
|
2030
|
+
name.title = theme.name;
|
|
2031
|
+
const desc = make("span", "dsh-tw-settings-muted", theme.description || theme.name);
|
|
2032
|
+
const row = make("div", "dsh-tw-settings-row dsh-tw-settings-plugin");
|
|
2033
|
+
row.append(load, act, name, desc);
|
|
2034
|
+
themeWrap.append(row);
|
|
2035
|
+
}
|
|
2036
|
+
const applyThemes = make("button", "dsh-tw-settings-btn dsh-tw-settings-primary", "应用主题(重启 TW)");
|
|
2037
|
+
applyThemes.type = "button";
|
|
2038
|
+
applyThemes.addEventListener("click", () => {
|
|
2039
|
+
applyThemes.disabled = true;
|
|
2040
|
+
(async () => {
|
|
2041
|
+
try {
|
|
2042
|
+
await fetchJson(INFO_ENDPOINT, {
|
|
2043
|
+
method: "POST",
|
|
2044
|
+
headers: { "content-type": "application/json" },
|
|
2045
|
+
body: JSON.stringify({
|
|
2046
|
+
themes: [...loadedThemes],
|
|
2047
|
+
themeActive: activeThemeName
|
|
2048
|
+
})
|
|
2049
|
+
});
|
|
2050
|
+
toast("主题已应用,TW 已重启");
|
|
2051
|
+
refresh();
|
|
2052
|
+
} catch (err) {
|
|
2053
|
+
toast(`应用失败:${err instanceof Error ? err.message : String(err)}`);
|
|
2054
|
+
applyThemes.disabled = false;
|
|
2055
|
+
}
|
|
2056
|
+
})();
|
|
2057
|
+
});
|
|
2058
|
+
themeSection.append(themeHint, themeHead, themeWrap, applyThemes);
|
|
2059
|
+
body.append(themeSection);
|
|
2060
|
+
const langSection = make("section", "dsh-tw-settings-section");
|
|
2061
|
+
langSection.append(make("h3", "dsh-tw-settings-h", "语言管理(自带官方语言包)"));
|
|
2062
|
+
const langHint = make("div", "dsh-tw-settings-muted", "勾选启用语言插件并重启 TW;如中文请选 zh-Hans(简体)或 zh-CN。");
|
|
2063
|
+
const langWrap = make("div", "dsh-tw-settings-list");
|
|
2064
|
+
for (const lang of languages) {
|
|
2065
|
+
const input = make("input", "dsh-tw-settings-check");
|
|
2066
|
+
input.type = "checkbox";
|
|
2067
|
+
input.checked = activeLanguages.has(lang.name);
|
|
2068
|
+
input.addEventListener("change", () => {
|
|
2069
|
+
if (input.checked) activeLanguages.add(lang.name);
|
|
2070
|
+
else activeLanguages.delete(lang.name);
|
|
2071
|
+
});
|
|
2072
|
+
const label = make("label", "dsh-tw-settings-row dsh-tw-settings-plugin");
|
|
2073
|
+
const name = make("span", "dsh-tw-settings-name", lang.label);
|
|
2074
|
+
name.title = lang.name;
|
|
2075
|
+
const desc = make("span", "dsh-tw-settings-muted", lang.description || lang.name);
|
|
2076
|
+
label.append(input, name, desc);
|
|
2077
|
+
langWrap.append(label);
|
|
2078
|
+
}
|
|
2079
|
+
const applyLangs = make("button", "dsh-tw-settings-btn dsh-tw-settings-primary", "应用语言(重启 TW)");
|
|
2080
|
+
applyLangs.type = "button";
|
|
2081
|
+
applyLangs.addEventListener("click", () => {
|
|
2082
|
+
applyLangs.disabled = true;
|
|
2083
|
+
(async () => {
|
|
2084
|
+
try {
|
|
2085
|
+
await fetchJson(INFO_ENDPOINT, {
|
|
2086
|
+
method: "POST",
|
|
2087
|
+
headers: { "content-type": "application/json" },
|
|
2088
|
+
body: JSON.stringify({ languages: [...activeLanguages] })
|
|
2089
|
+
});
|
|
2090
|
+
toast("语言已应用,TW 已重启");
|
|
2091
|
+
refresh();
|
|
2092
|
+
} catch (err) {
|
|
2093
|
+
toast(`应用失败:${err instanceof Error ? err.message : String(err)}`);
|
|
2094
|
+
applyLangs.disabled = false;
|
|
2095
|
+
}
|
|
2096
|
+
})();
|
|
2097
|
+
});
|
|
2098
|
+
langSection.append(langHint, langWrap, applyLangs);
|
|
2099
|
+
body.append(langSection);
|
|
2100
|
+
}
|
|
2101
|
+
function renderMain(body, state, refresh) {
|
|
2102
|
+
body.replaceChildren();
|
|
2103
|
+
renderConfigSection(body, state.config ?? {}, refresh);
|
|
2104
|
+
renderCatalogSection(body, state.info, state.catalog, refresh);
|
|
2105
|
+
}
|
|
2106
|
+
/** React wrapper consumed by the shell's settings.section slot. */
|
|
2107
|
+
function SettingsSection() {
|
|
2108
|
+
const ref = react.useRef(null);
|
|
2109
|
+
react.useEffect(() => {
|
|
2110
|
+
const el = ref.current;
|
|
2111
|
+
return el === null ? void 0 : mountSettingsPage(el);
|
|
2112
|
+
}, []);
|
|
2113
|
+
return react.createElement("div", { ref });
|
|
2114
|
+
}
|
|
2115
|
+
//#endregion
|
|
2116
|
+
//#region src/client/index.ts
|
|
2117
|
+
/**
|
|
2118
|
+
* Browser half entry for dsh-tiddlywiki (design doc §12, D5): injects the
|
|
2119
|
+
* stylesheet, mounts the sidebar entry, the center-column TiddlyWiki panel,
|
|
2120
|
+
* the floating quick-note widget, and registers the plugin's settings page
|
|
2121
|
+
* (config panel, §13) into the shell's Settings.
|
|
2122
|
+
*
|
|
2123
|
+
* Failure policy: DOM mounting problems are logged, never thrown — the web
|
|
2124
|
+
* shell fails the whole boot when a plugin apply throws.
|
|
2125
|
+
*
|
|
2126
|
+
* Export shape: `name` / `inject` / `apply`, no default.
|
|
2127
|
+
*
|
|
2128
|
+
* @module dsh-tiddlywiki/client
|
|
2129
|
+
*/
|
|
2130
|
+
/** Client plugin name. */
|
|
2131
|
+
const name = "dsh-tiddlywiki/client";
|
|
2132
|
+
/** Required client services: the slots registry (settings.section seat). */
|
|
2133
|
+
const inject = ["slots"];
|
|
2134
|
+
/**
|
|
2135
|
+
* Client entry: installs styles and mounts the DOM seats + settings page.
|
|
2136
|
+
* @param ctx - the cordis client context.
|
|
2137
|
+
*/
|
|
2138
|
+
function apply(ctx) {
|
|
2139
|
+
try {
|
|
2140
|
+
injectStyles();
|
|
2141
|
+
const state = new PanelState();
|
|
2142
|
+
const disposers = [];
|
|
2143
|
+
try {
|
|
2144
|
+
disposers.push(mountSidebarEntry(state));
|
|
2145
|
+
disposers.push(mountPanel(state));
|
|
2146
|
+
disposers.push(mountNoteWidget());
|
|
2147
|
+
disposers.push(mountSyncButton());
|
|
2148
|
+
disposers.push(disposeEditorPopup);
|
|
2149
|
+
} catch (error) {
|
|
2150
|
+
console.error("[dsh-tiddlywiki] mount failed:", error);
|
|
2151
|
+
}
|
|
2152
|
+
try {
|
|
2153
|
+
const removeSettings = ctx.slots?.inject("settings.section", () => ctx.slots?.register({
|
|
2154
|
+
name: "settings.section",
|
|
2155
|
+
id: "dsh-tiddlywiki",
|
|
2156
|
+
order: 50,
|
|
2157
|
+
label: "TiddlyWiki 知识库"
|
|
2158
|
+
}, SettingsSection));
|
|
2159
|
+
if (removeSettings !== void 0) disposers.push(removeSettings);
|
|
2160
|
+
} catch (error) {
|
|
2161
|
+
console.error("[dsh-tiddlywiki] settings section failed:", error);
|
|
2162
|
+
}
|
|
2163
|
+
ctx.effect?.(() => () => {
|
|
2164
|
+
for (const dispose of disposers.splice(0)) dispose();
|
|
2165
|
+
}, "dsh-tiddlywiki: client mount");
|
|
2166
|
+
} catch (error) {
|
|
2167
|
+
console.error("[dsh-tiddlywiki] client half failed to start:", error);
|
|
2168
|
+
}
|
|
2169
|
+
}
|
|
2170
|
+
//#endregion
|
|
2171
|
+
exports.apply = apply;
|
|
2172
|
+
exports.inject = inject;
|
|
2173
|
+
exports.name = name;
|