@restregular/dsh-notepad 0.0.0-stage → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +129 -2
- package/client.js +2072 -0
- package/cordis.patch.yml +9 -0
- package/icon.svg +19 -0
- package/index.js +268 -0
- package/lib/note-store.js +271 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +69 -4
package/client.js
ADDED
|
@@ -0,0 +1,2072 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-notepad — Client half.
|
|
3
|
+
*
|
|
4
|
+
* Registers one floating notepad into `shell.overlay`. The panel is scoped to
|
|
5
|
+
* the workspace of the session currently retained by the main view, so every
|
|
6
|
+
* session opened from the same workspace shows the same note.
|
|
7
|
+
*
|
|
8
|
+
* The note is a list of segments rendered as cards in a scrollable list:
|
|
9
|
+
* - Enter splits the current segment into a new card; Shift+Enter is a
|
|
10
|
+
* normal line break;
|
|
11
|
+
* - every card can be inserted, replace the composer, or be sent directly;
|
|
12
|
+
* - sending archives the segment automatically, and only archived segments
|
|
13
|
+
* can be deleted;
|
|
14
|
+
* - active segments can be multi-selected for batch insert / replace /
|
|
15
|
+
* send / archive, with one-click select-all.
|
|
16
|
+
*
|
|
17
|
+
* Data lives on the Host side at `<workspace>/.dsh/notepad.json` through the
|
|
18
|
+
* same-origin route `/dsh-notepad/api/note`; the widget's position, size,
|
|
19
|
+
* collapsed/hidden state and archive visibility are kept in localStorage.
|
|
20
|
+
*
|
|
21
|
+
* The whole file is wrapped in an IIFE: client bundles from every plugin are
|
|
22
|
+
* concatenated into the same classic-script realm, so top-level bindings would
|
|
23
|
+
* leak into a shared lexical scope and throw on reload.
|
|
24
|
+
*/
|
|
25
|
+
(() => {
|
|
26
|
+
const MODULE_ID = '@restregular/dsh-notepad';
|
|
27
|
+
|
|
28
|
+
/** Whether the host facade already holds a factory for this module id. */
|
|
29
|
+
function alreadyRegistered() {
|
|
30
|
+
const factories = window.__ModuleLoader__?.factories;
|
|
31
|
+
return factories !== undefined && typeof factories.has === 'function' && factories.has(MODULE_ID);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (!alreadyRegistered()) {
|
|
35
|
+
window.__ModuleLoader__.load({
|
|
36
|
+
id: MODULE_ID,
|
|
37
|
+
factory: (require) => {
|
|
38
|
+
const React = require('react');
|
|
39
|
+
const h = React.createElement;
|
|
40
|
+
const { useState, useEffect, useRef } = React;
|
|
41
|
+
|
|
42
|
+
const NS = 'dsh-notepad';
|
|
43
|
+
const API = '/dsh-notepad/api';
|
|
44
|
+
const UI_KEY = 'dsh-notepad.ui.v1';
|
|
45
|
+
const DEFAULT_W = 420;
|
|
46
|
+
const DEFAULT_H = 460;
|
|
47
|
+
const MIN_W = 320;
|
|
48
|
+
const MIN_H = 240;
|
|
49
|
+
const HEAD_H = 44;
|
|
50
|
+
const SAVE_DELAY = 500;
|
|
51
|
+
const MAX_SEGMENTS = 200;
|
|
52
|
+
const MAX_SEGMENT_CHARS = 160000;
|
|
53
|
+
|
|
54
|
+
// ------------------------------------------------------------------
|
|
55
|
+
// Copy
|
|
56
|
+
// ------------------------------------------------------------------
|
|
57
|
+
const DICT = {
|
|
58
|
+
zh: {
|
|
59
|
+
title: '记事本',
|
|
60
|
+
editor: '分段内容',
|
|
61
|
+
placeholder: '记录开发中遇到的问题,或下一阶段要做的事……',
|
|
62
|
+
segmentPlaceholder: '新分段…',
|
|
63
|
+
loading: '读取中…',
|
|
64
|
+
empty: '还没有内容',
|
|
65
|
+
unsaved: '未保存',
|
|
66
|
+
saving: '保存中…',
|
|
67
|
+
saved: '已保存',
|
|
68
|
+
saveFailed: '保存失败',
|
|
69
|
+
loadFailed: '读取失败',
|
|
70
|
+
retry: '重试',
|
|
71
|
+
insert: '插入到输入框',
|
|
72
|
+
insertSegment: '插入此段',
|
|
73
|
+
insertSelected: '插入选中的 {n} 段',
|
|
74
|
+
inserted: '已插入输入框',
|
|
75
|
+
insertEmpty: '没有可插入的内容',
|
|
76
|
+
insertUnavailable: '输入框暂不可用,内容已复制',
|
|
77
|
+
replace: '替换输入框内容',
|
|
78
|
+
replaceSegment: '替换此段',
|
|
79
|
+
replaceSelected: '替换为选中的 {n} 段',
|
|
80
|
+
replaced: '已替换输入框内容',
|
|
81
|
+
replaceUnavailable: '替换不可用,内容已复制',
|
|
82
|
+
send: '发送',
|
|
83
|
+
sendSegment: '发送此段',
|
|
84
|
+
sendSelected: '发送选中的 {n} 段',
|
|
85
|
+
sending: '发送中…',
|
|
86
|
+
sent: '已发送',
|
|
87
|
+
sendFailed: '发送失败',
|
|
88
|
+
sendUnavailable: '发送接口不可用,已插入输入框',
|
|
89
|
+
copy: '复制全部',
|
|
90
|
+
copied: '已复制',
|
|
91
|
+
copyFailed: '复制失败',
|
|
92
|
+
selectAll: '全选',
|
|
93
|
+
clearSelection: '取消选择',
|
|
94
|
+
selectHint: '勾选分段可批量操作',
|
|
95
|
+
selected: '已选 {n} 段',
|
|
96
|
+
selectSegment: '选择此段',
|
|
97
|
+
archive: '归档',
|
|
98
|
+
archiveSegment: '归档此段',
|
|
99
|
+
archiveSelected: '归档选中的 {n} 段',
|
|
100
|
+
archived: '归档',
|
|
101
|
+
archivedToast: '已归档,可在归档中查看',
|
|
102
|
+
archivedCount: '已归档 {n} 段',
|
|
103
|
+
showArchive: '显示归档({n})',
|
|
104
|
+
hideArchive: '隐藏归档({n})',
|
|
105
|
+
archiveCount: '归档 · {n}',
|
|
106
|
+
archiveEmpty: '还没有归档的分段',
|
|
107
|
+
noActive: '没有进行中的分段',
|
|
108
|
+
restore: '恢复',
|
|
109
|
+
restoreSegment: '恢复此段',
|
|
110
|
+
restoreAll: '全部恢复',
|
|
111
|
+
restoredToast: '已恢复',
|
|
112
|
+
restoredCount: '已恢复 {n} 段',
|
|
113
|
+
deleteSegment: '删除此段',
|
|
114
|
+
deletedToast: '已删除',
|
|
115
|
+
clearArchive: '清空归档',
|
|
116
|
+
clearedArchive: '已清空 {n} 段归档',
|
|
117
|
+
undo: '撤销',
|
|
118
|
+
addSegment: '新建分段',
|
|
119
|
+
tooManySegments: '最多 {n} 段',
|
|
120
|
+
emptySegment: '(空分段)',
|
|
121
|
+
collapse: '折叠',
|
|
122
|
+
expand: '展开',
|
|
123
|
+
minimize: '最小化',
|
|
124
|
+
show: '显示记事本(Ctrl+Shift+N)',
|
|
125
|
+
chars: '{n} 字符',
|
|
126
|
+
segments: '{n} 段',
|
|
127
|
+
hint: 'Enter 新建分段 · Shift+Enter 换行 · Ctrl+Enter 插入',
|
|
128
|
+
workspace: '工作区',
|
|
129
|
+
resize: '拖动调整大小',
|
|
130
|
+
},
|
|
131
|
+
en: {
|
|
132
|
+
title: 'Notepad',
|
|
133
|
+
editor: 'Segment content',
|
|
134
|
+
placeholder: 'Jot down problems, or what to build next…',
|
|
135
|
+
segmentPlaceholder: 'New segment…',
|
|
136
|
+
loading: 'Loading…',
|
|
137
|
+
empty: 'Nothing here yet',
|
|
138
|
+
unsaved: 'Unsaved',
|
|
139
|
+
saving: 'Saving…',
|
|
140
|
+
saved: 'Saved',
|
|
141
|
+
saveFailed: 'Save failed',
|
|
142
|
+
loadFailed: 'Load failed',
|
|
143
|
+
retry: 'Retry',
|
|
144
|
+
insert: 'Insert into composer',
|
|
145
|
+
insertSegment: 'Insert this segment',
|
|
146
|
+
insertSelected: 'Insert {n} selected',
|
|
147
|
+
inserted: 'Inserted into composer',
|
|
148
|
+
insertEmpty: 'Nothing to insert',
|
|
149
|
+
insertUnavailable: 'Composer unavailable; copied instead',
|
|
150
|
+
replace: 'Replace composer',
|
|
151
|
+
replaceSegment: 'Replace with this segment',
|
|
152
|
+
replaceSelected: 'Replace with {n} selected',
|
|
153
|
+
replaced: 'Composer replaced',
|
|
154
|
+
replaceUnavailable: 'Replace unavailable; copied instead',
|
|
155
|
+
send: 'Send',
|
|
156
|
+
sendSegment: 'Send this segment',
|
|
157
|
+
sendSelected: 'Send {n} selected',
|
|
158
|
+
sending: 'Sending…',
|
|
159
|
+
sent: 'Sent',
|
|
160
|
+
sendFailed: 'Send failed',
|
|
161
|
+
sendUnavailable: 'Send unavailable; inserted into composer',
|
|
162
|
+
copy: 'Copy all',
|
|
163
|
+
copied: 'Copied',
|
|
164
|
+
copyFailed: 'Copy failed',
|
|
165
|
+
selectAll: 'Select all',
|
|
166
|
+
clearSelection: 'Clear selection',
|
|
167
|
+
selectHint: 'Check segments to batch',
|
|
168
|
+
selected: '{n} selected',
|
|
169
|
+
selectSegment: 'Select this segment',
|
|
170
|
+
archive: 'Archive',
|
|
171
|
+
archiveSegment: 'Archive this segment',
|
|
172
|
+
archiveSelected: 'Archive {n} selected',
|
|
173
|
+
archived: 'Archived',
|
|
174
|
+
archivedToast: 'Archived; available under Archive',
|
|
175
|
+
archivedCount: 'Archived {n}',
|
|
176
|
+
showArchive: 'Show archive ({n})',
|
|
177
|
+
hideArchive: 'Hide archive ({n})',
|
|
178
|
+
archiveCount: 'Archive · {n}',
|
|
179
|
+
archiveEmpty: 'Nothing archived yet',
|
|
180
|
+
noActive: 'No active segments',
|
|
181
|
+
restore: 'Restore',
|
|
182
|
+
restoreSegment: 'Restore this segment',
|
|
183
|
+
restoreAll: 'Restore all',
|
|
184
|
+
restoredToast: 'Restored',
|
|
185
|
+
restoredCount: 'Restored {n}',
|
|
186
|
+
deleteSegment: 'Delete this segment',
|
|
187
|
+
deletedToast: 'Deleted',
|
|
188
|
+
clearArchive: 'Clear archive',
|
|
189
|
+
clearedArchive: 'Cleared {n} archived',
|
|
190
|
+
undo: 'Undo',
|
|
191
|
+
addSegment: 'New segment',
|
|
192
|
+
tooManySegments: 'At most {n} segments',
|
|
193
|
+
emptySegment: '(empty segment)',
|
|
194
|
+
collapse: 'Collapse',
|
|
195
|
+
expand: 'Expand',
|
|
196
|
+
minimize: 'Minimize',
|
|
197
|
+
show: 'Show notepad (Ctrl+Shift+N)',
|
|
198
|
+
chars: '{n} chars',
|
|
199
|
+
segments: '{n} segments',
|
|
200
|
+
hint: 'Enter new segment · Shift+Enter newline · Ctrl+Enter insert',
|
|
201
|
+
workspace: 'Workspace',
|
|
202
|
+
resize: 'Drag to resize',
|
|
203
|
+
},
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
function preferredLanguage() {
|
|
207
|
+
try {
|
|
208
|
+
const tag = String((typeof navigator !== 'undefined' && navigator.language) || 'zh').toLowerCase();
|
|
209
|
+
return tag.startsWith('zh') ? 'zh' : 'en';
|
|
210
|
+
} catch {
|
|
211
|
+
return 'zh';
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function fallbackTranslate() {
|
|
216
|
+
return (key, params) => {
|
|
217
|
+
const dict = DICT[preferredLanguage()] ?? DICT.zh;
|
|
218
|
+
let out = dict[key] ?? DICT.zh[key] ?? key;
|
|
219
|
+
if (params !== undefined && params !== null) {
|
|
220
|
+
out = out.replace(/\{(\w+)\}/gu, (match, name) =>
|
|
221
|
+
Object.prototype.hasOwnProperty.call(params, name) ? String(params[name]) : match);
|
|
222
|
+
}
|
|
223
|
+
return out;
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// ------------------------------------------------------------------
|
|
228
|
+
// Icons
|
|
229
|
+
// ------------------------------------------------------------------
|
|
230
|
+
function Svg({ size = 16, children, viewBox = '0 0 16 16' }) {
|
|
231
|
+
return h(
|
|
232
|
+
'svg',
|
|
233
|
+
{
|
|
234
|
+
width: size,
|
|
235
|
+
height: size,
|
|
236
|
+
viewBox,
|
|
237
|
+
fill: 'none',
|
|
238
|
+
'aria-hidden': 'true',
|
|
239
|
+
focusable: 'false',
|
|
240
|
+
},
|
|
241
|
+
children,
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function NotepadIcon({ size = 16 }) {
|
|
246
|
+
return h(
|
|
247
|
+
Svg,
|
|
248
|
+
{ size, viewBox: '0 0 16 16' },
|
|
249
|
+
h('rect', {
|
|
250
|
+
x: 2.6, y: 1.9, width: 10.8, height: 12.2, rx: 2.2,
|
|
251
|
+
stroke: 'currentColor', strokeWidth: 1.25,
|
|
252
|
+
}),
|
|
253
|
+
h('path', {
|
|
254
|
+
d: 'M5.4 5.5h5.2M5.4 8h5.2M5.4 10.5h3',
|
|
255
|
+
stroke: 'currentColor', strokeWidth: 1.25, strokeLinecap: 'round',
|
|
256
|
+
}),
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/** Arrow pointing to the bottom-left corner. */
|
|
261
|
+
function InsertIcon({ size = 16 }) {
|
|
262
|
+
return h(
|
|
263
|
+
Svg,
|
|
264
|
+
{ size, viewBox: '0 0 16 16' },
|
|
265
|
+
h('path', {
|
|
266
|
+
d: 'M13.2 2.8 3.6 12.4',
|
|
267
|
+
stroke: 'currentColor', strokeWidth: 1.4, strokeLinecap: 'round',
|
|
268
|
+
}),
|
|
269
|
+
h('path', {
|
|
270
|
+
d: 'M3.4 7.2v5.4h5.4',
|
|
271
|
+
stroke: 'currentColor', strokeWidth: 1.4,
|
|
272
|
+
strokeLinecap: 'round', strokeLinejoin: 'round',
|
|
273
|
+
}),
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/** Two opposing arrows: replace the composer content. */
|
|
278
|
+
function ReplaceIcon({ size = 16 }) {
|
|
279
|
+
return h(
|
|
280
|
+
Svg,
|
|
281
|
+
{ size, viewBox: '0 0 16 16' },
|
|
282
|
+
h('path', {
|
|
283
|
+
d: 'M2.6 5.4h8.2M8.8 3.2l2.2 2.2-2.2 2.2',
|
|
284
|
+
stroke: 'currentColor', strokeWidth: 1.35,
|
|
285
|
+
strokeLinecap: 'round', strokeLinejoin: 'round',
|
|
286
|
+
}),
|
|
287
|
+
h('path', {
|
|
288
|
+
d: 'M13.4 10.6H5.2M7.2 8.4 5 10.6l2.2 2.2',
|
|
289
|
+
stroke: 'currentColor', strokeWidth: 1.35,
|
|
290
|
+
strokeLinecap: 'round', strokeLinejoin: 'round',
|
|
291
|
+
}),
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/** Outline paper plane. */
|
|
296
|
+
function SendIcon({ size = 16 }) {
|
|
297
|
+
return h(
|
|
298
|
+
Svg,
|
|
299
|
+
{ size, viewBox: '0 0 16 16' },
|
|
300
|
+
h('path', {
|
|
301
|
+
d: 'M13.9 2.1 2.3 6.4l4.7 2.3L13.9 2.1Z',
|
|
302
|
+
stroke: 'currentColor', strokeWidth: 1.25,
|
|
303
|
+
strokeLinecap: 'round', strokeLinejoin: 'round',
|
|
304
|
+
}),
|
|
305
|
+
h('path', {
|
|
306
|
+
d: 'M13.9 2.1 9.4 13.7 7 8.7',
|
|
307
|
+
stroke: 'currentColor', strokeWidth: 1.25,
|
|
308
|
+
strokeLinecap: 'round', strokeLinejoin: 'round',
|
|
309
|
+
}),
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function CopyIcon({ size = 16 }) {
|
|
314
|
+
return h(
|
|
315
|
+
Svg,
|
|
316
|
+
{ size, viewBox: '0 0 16 16' },
|
|
317
|
+
h('rect', {
|
|
318
|
+
x: 5.2, y: 5.2, width: 8, height: 8, rx: 1.9,
|
|
319
|
+
stroke: 'currentColor', strokeWidth: 1.3,
|
|
320
|
+
}),
|
|
321
|
+
h('path', {
|
|
322
|
+
d: 'M10.8 3.4c0-.7-.6-1.3-1.3-1.3H4.2c-.7 0-1.3.6-1.3 1.3v5.3c0 .7.6 1.3 1.3 1.3',
|
|
323
|
+
stroke: 'currentColor', strokeWidth: 1.3, strokeLinecap: 'round',
|
|
324
|
+
}),
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function ArchiveIcon({ size = 16 }) {
|
|
329
|
+
return h(
|
|
330
|
+
Svg,
|
|
331
|
+
{ size, viewBox: '0 0 16 16' },
|
|
332
|
+
h('path', {
|
|
333
|
+
d: 'M2.2 3.2h11.6v2.8H2.2z',
|
|
334
|
+
stroke: 'currentColor', strokeWidth: 1.25,
|
|
335
|
+
strokeLinejoin: 'round',
|
|
336
|
+
}),
|
|
337
|
+
h('path', {
|
|
338
|
+
d: 'M3.2 6v6.1c0 .7.6 1.3 1.3 1.3h7c.7 0 1.3-.6 1.3-1.3V6',
|
|
339
|
+
stroke: 'currentColor', strokeWidth: 1.25, strokeLinecap: 'round',
|
|
340
|
+
}),
|
|
341
|
+
h('path', {
|
|
342
|
+
d: 'M6.2 8.6h3.6',
|
|
343
|
+
stroke: 'currentColor', strokeWidth: 1.25, strokeLinecap: 'round',
|
|
344
|
+
}),
|
|
345
|
+
);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function RestoreIcon({ size = 16 }) {
|
|
349
|
+
return h(
|
|
350
|
+
Svg,
|
|
351
|
+
{ size, viewBox: '0 0 16 16' },
|
|
352
|
+
h('path', {
|
|
353
|
+
d: 'M8 10.2V3.2M5.6 5.6 8 3.2l2.4 2.4',
|
|
354
|
+
stroke: 'currentColor', strokeWidth: 1.35,
|
|
355
|
+
strokeLinecap: 'round', strokeLinejoin: 'round',
|
|
356
|
+
}),
|
|
357
|
+
h('path', {
|
|
358
|
+
d: 'M3.1 9.8v1.5c0 .9.7 1.6 1.6 1.6h6.6c.9 0 1.6-.7 1.6-1.6V9.8',
|
|
359
|
+
stroke: 'currentColor', strokeWidth: 1.35, strokeLinecap: 'round',
|
|
360
|
+
}),
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function TrashIcon({ size = 16 }) {
|
|
365
|
+
return h(
|
|
366
|
+
Svg,
|
|
367
|
+
{ size, viewBox: '0 0 16 16' },
|
|
368
|
+
h('path', {
|
|
369
|
+
d: 'M2.8 4.4h10.4',
|
|
370
|
+
stroke: 'currentColor', strokeWidth: 1.3, strokeLinecap: 'round',
|
|
371
|
+
}),
|
|
372
|
+
h('path', {
|
|
373
|
+
d: 'M6.1 4.4V3.2c0-.5.4-.9.9-.9h2c.5 0 .9.4.9.9v1.2',
|
|
374
|
+
stroke: 'currentColor', strokeWidth: 1.3, strokeLinecap: 'round',
|
|
375
|
+
}),
|
|
376
|
+
h('path', {
|
|
377
|
+
d: 'M4.3 4.4 4.9 13c0 .6.5 1 1.1 1h4c.6 0 1.1-.4 1.1-1l.6-8.6',
|
|
378
|
+
stroke: 'currentColor', strokeWidth: 1.3, strokeLinecap: 'round',
|
|
379
|
+
}),
|
|
380
|
+
);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function PlusIcon({ size = 16 }) {
|
|
384
|
+
return h(
|
|
385
|
+
Svg,
|
|
386
|
+
{ size, viewBox: '0 0 16 16' },
|
|
387
|
+
h('path', {
|
|
388
|
+
d: 'M8 3.2v9.6M3.2 8h9.6',
|
|
389
|
+
stroke: 'currentColor', strokeWidth: 1.4, strokeLinecap: 'round',
|
|
390
|
+
}),
|
|
391
|
+
);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function CloseIcon({ size = 16 }) {
|
|
395
|
+
return h(
|
|
396
|
+
Svg,
|
|
397
|
+
{ size, viewBox: '0 0 16 16' },
|
|
398
|
+
h('path', {
|
|
399
|
+
d: 'm4.2 4.2 7.6 7.6M11.8 4.2l-7.6 7.6',
|
|
400
|
+
stroke: 'currentColor', strokeWidth: 1.4, strokeLinecap: 'round',
|
|
401
|
+
}),
|
|
402
|
+
);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/** Chevrons folded towards each other (collapse). */
|
|
406
|
+
function ChevronsCollapseIcon({ size = 16 }) {
|
|
407
|
+
return h(
|
|
408
|
+
Svg,
|
|
409
|
+
{ size, viewBox: '0 0 16 16' },
|
|
410
|
+
h('path', {
|
|
411
|
+
d: 'M4.2 5 8 8.8 11.8 5',
|
|
412
|
+
stroke: 'currentColor', strokeWidth: 1.35,
|
|
413
|
+
strokeLinecap: 'round', strokeLinejoin: 'round',
|
|
414
|
+
}),
|
|
415
|
+
h('path', {
|
|
416
|
+
d: 'M4.2 11 8 7.2 11.8 11',
|
|
417
|
+
stroke: 'currentColor', strokeWidth: 1.35,
|
|
418
|
+
strokeLinecap: 'round', strokeLinejoin: 'round',
|
|
419
|
+
}),
|
|
420
|
+
);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/** Chevrons opening away from each other (expand). */
|
|
424
|
+
function ChevronsExpandIcon({ size = 16 }) {
|
|
425
|
+
return h(
|
|
426
|
+
Svg,
|
|
427
|
+
{ size, viewBox: '0 0 16 16' },
|
|
428
|
+
h('path', {
|
|
429
|
+
d: 'M4.2 8.8 8 5 11.8 8.8',
|
|
430
|
+
stroke: 'currentColor', strokeWidth: 1.35,
|
|
431
|
+
strokeLinecap: 'round', strokeLinejoin: 'round',
|
|
432
|
+
}),
|
|
433
|
+
h('path', {
|
|
434
|
+
d: 'M4.2 7.2 8 11 11.8 7.2',
|
|
435
|
+
stroke: 'currentColor', strokeWidth: 1.35,
|
|
436
|
+
strokeLinecap: 'round', strokeLinejoin: 'round',
|
|
437
|
+
}),
|
|
438
|
+
);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/** Arrow towards the bottom-right corner: minimize to the corner icon. */
|
|
442
|
+
function MinimizeIcon({ size = 16 }) {
|
|
443
|
+
return h(
|
|
444
|
+
Svg,
|
|
445
|
+
{ size, viewBox: '0 0 16 16' },
|
|
446
|
+
h('path', {
|
|
447
|
+
d: 'M4.2 4.2 11.4 11.4',
|
|
448
|
+
stroke: 'currentColor', strokeWidth: 1.35, strokeLinecap: 'round',
|
|
449
|
+
}),
|
|
450
|
+
h('path', {
|
|
451
|
+
d: 'M6.8 11.6h4.8V6.8',
|
|
452
|
+
stroke: 'currentColor', strokeWidth: 1.35,
|
|
453
|
+
strokeLinecap: 'round', strokeLinejoin: 'round',
|
|
454
|
+
}),
|
|
455
|
+
);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// ------------------------------------------------------------------
|
|
459
|
+
// Styles
|
|
460
|
+
// ------------------------------------------------------------------
|
|
461
|
+
const FONT_STACK = 'var(--dsw-font-family,-apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC","Hiragino Sans GB","Microsoft YaHei","Helvetica Neue",Helvetica,Arial,sans-serif)';
|
|
462
|
+
const CSS = `.dsh-np-card{--np-accent:var(--dsw-alias-state-business-primary,#4176e6);--np-accent-fg:#fff;--np-surface:var(--dsw-alias-bg-layer-1,#fff);--np-surface-2:var(--dsw-alias-bg-layer-2,#fff);position:fixed;z-index:30;display:flex;flex-direction:column;box-sizing:border-box;width:${DEFAULT_W}px;max-width:calc(100vw - 16px);height:${DEFAULT_H}px;border:1px solid var(--dsw-alias-border-l2,rgb(15 23 42/.12));border-radius:14px;background:var(--np-surface);color:var(--dsw-alias-label-primary,#1f2430);box-shadow:var(--dsw-elevation-prominent,0 20px 50px rgb(15 23 42/.22),0 2px 8px rgb(15 23 42/.08));overflow:hidden;backdrop-filter:saturate(1.2) blur(10px);font-family:${FONT_STACK};font-size:13px;line-height:1.5;transition:height .2s ease,opacity .18s ease,transform .18s ease}
|
|
463
|
+
.dsh-np-card[data-hidden=true]{opacity:0;visibility:hidden;pointer-events:none;transform:translateY(10px) scale(.97);transition:opacity .18s ease,transform .18s ease,visibility 0s linear .18s}
|
|
464
|
+
.dsh-np-card[data-resizing=true]{transition:none;user-select:none}
|
|
465
|
+
.dsh-np-card[data-minimized=true]{border-bottom-color:transparent}
|
|
466
|
+
.dsh-np-head{display:flex;align-items:center;gap:7px;flex:none;height:${HEAD_H}px;padding:0 8px 0 12px;box-sizing:border-box;border-bottom:1px solid var(--dsw-alias-border-l1,rgb(15 23 42/.08));cursor:grab;user-select:none;touch-action:none;background:var(--np-surface);background:linear-gradient(180deg,var(--np-surface),color-mix(in srgb,var(--np-surface-2) 72%,var(--np-surface)))}
|
|
467
|
+
.dsh-np-card[data-minimized=true] .dsh-np-head{border-bottom-color:transparent}
|
|
468
|
+
.dsh-np-head:active{cursor:grabbing}
|
|
469
|
+
.dsh-np-dot{flex:none;width:7px;height:7px;border-radius:50%;background:var(--dsw-alias-label-caption,#9aa3b2);transition:background .15s}
|
|
470
|
+
.dsh-np-dot[data-state=dirty],.dsh-np-dot[data-state=saving]{background:var(--dsw-alias-state-warn-primary,#f5a524)}
|
|
471
|
+
.dsh-np-dot[data-state=saving]{animation:dsh-np-pulse 1s ease-in-out infinite}
|
|
472
|
+
.dsh-np-dot[data-state=error]{background:var(--dsw-alias-state-error-primary,#e25555)}
|
|
473
|
+
.dsh-np-dot[data-state=saved]{background:var(--dsw-alias-state-success-primary,#2fb36d)}
|
|
474
|
+
.dsh-np-title{flex:none;font-size:12.5px;font-weight:650;letter-spacing:.01em;white-space:nowrap}
|
|
475
|
+
.dsh-np-workspace{min-width:0;max-width:104px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;padding:1px 7px;border-radius:6px;background:var(--dsw-alias-interactive-bg-hover,rgb(38 49 72/.06));color:var(--dsw-alias-label-tertiary,#6b7280);font-size:10.5px;line-height:16px;flex:0 1 auto}
|
|
476
|
+
.dsh-np-spacer{flex:1 1 auto;min-width:0}
|
|
477
|
+
.dsh-np-btn{position:relative;flex:none;display:inline-flex;align-items:center;justify-content:center;width:27px;height:27px;padding:0;border:0;border-radius:7px;background:transparent;color:var(--dsw-alias-label-tertiary,#6b7280);cursor:pointer;transition:background .12s,color .12s,opacity .12s}
|
|
478
|
+
.dsh-np-btn:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover,rgb(38 49 72/.06));color:var(--dsw-alias-label-primary,#1f2430)}
|
|
479
|
+
.dsh-np-btn[data-active=true]{background:color-mix(in srgb,var(--np-accent) 12%,transparent);color:var(--np-accent)}
|
|
480
|
+
.dsh-np-btn[data-accent=true]{background:var(--np-accent);color:var(--np-accent-fg)}
|
|
481
|
+
.dsh-np-btn[data-accent=true]:hover:not(:disabled){background:color-mix(in srgb,var(--np-accent) 88%,#000);color:var(--np-accent-fg)}
|
|
482
|
+
.dsh-np-btn:disabled{opacity:.38;cursor:default;background:transparent;color:var(--dsw-alias-label-tertiary,#6b7280)}
|
|
483
|
+
.dsh-np-btn:focus-visible,.dsh-np-segbtn:focus-visible,.dsh-np-link:focus-visible,.dsh-np-empty-add:focus-visible,.dsh-np-toast-action:focus-visible,.dsh-np-selectall input:focus-visible,.dsh-np-check input:focus-visible,.dsh-np-restore:focus-visible{outline:2px solid var(--dsw-focus-ring-color,var(--dsw-alias-state-business-primary,#4176e6));outline-offset:-2px}
|
|
484
|
+
.dsh-np-badge{position:absolute;top:2px;right:2px;min-width:13px;height:13px;padding:0 3px;box-sizing:border-box;border-radius:7px;background:var(--np-accent);color:var(--np-accent-fg);font-size:9px;font-weight:600;line-height:13px;text-align:center;font-variant-numeric:tabular-nums}
|
|
485
|
+
.dsh-np-body{display:flex;flex:1 1 auto;flex-direction:column;min-height:0;padding:8px 10px 0;opacity:1;transition:opacity .16s ease,visibility 0s linear 0s}
|
|
486
|
+
.dsh-np-card[data-minimized=true] .dsh-np-body{opacity:0;visibility:hidden;pointer-events:none;transition:opacity .16s ease,visibility 0s linear .16s}
|
|
487
|
+
.dsh-np-tools{display:flex;align-items:center;gap:4px;flex:none;padding:0 2px 7px;border-bottom:1px solid var(--dsw-alias-border-l1,rgb(15 23 42/.08))}
|
|
488
|
+
.dsh-np-selectall{display:inline-flex;align-items:center;gap:6px;min-width:0;cursor:pointer;color:var(--dsw-alias-label-tertiary,#6b7280);font-size:11px;white-space:nowrap}
|
|
489
|
+
.dsh-np-selectall input{width:14px;height:14px;margin:0;accent-color:var(--np-accent);cursor:pointer;flex:none}
|
|
490
|
+
.dsh-np-selectall input:disabled{cursor:default;opacity:.45}
|
|
491
|
+
.dsh-np-selcount{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:150px}
|
|
492
|
+
.dsh-np-list{display:flex;flex:1 1 auto;flex-direction:column;gap:8px;min-height:0;padding:8px 2px 8px 0;overflow-y:auto;overscroll-behavior:contain;--dsh-scrollbar-thumb:var(--dsw-alias-scrollbar-bg-l2);--dsh-scrollbar-thumb-hover:var(--dsw-alias-scrollbar-hover-l2)}
|
|
493
|
+
.dsh-np-seg{display:flex;flex-direction:column;flex:none;border:1px solid var(--dsw-alias-border-l2,rgb(15 23 42/.12));border-radius:10px;background:var(--np-surface-2);transition:border-color .12s,background .12s,opacity .12s;animation:dsh-np-item-in .16s ease-out}
|
|
494
|
+
.dsh-np-seg[data-selected=true]{border-color:var(--np-accent);background:color-mix(in srgb,var(--np-accent) 6%,var(--np-surface-2));box-shadow:0 0 0 1px var(--np-accent) inset}
|
|
495
|
+
.dsh-np-seg[data-archived=true]{border-style:dashed;background:transparent;opacity:.82;animation:none}
|
|
496
|
+
.dsh-np-seg-head{display:flex;align-items:center;gap:3px;flex:none;min-height:30px;padding:3px 5px 0 7px;box-sizing:border-box}
|
|
497
|
+
.dsh-np-check{display:inline-flex;align-items:center;justify-content:center;flex:none;width:20px;height:20px;cursor:pointer}
|
|
498
|
+
.dsh-np-check input{width:14px;height:14px;margin:0;accent-color:var(--np-accent);cursor:pointer}
|
|
499
|
+
.dsh-np-index{flex:none;color:var(--dsw-alias-label-caption,#9aa3b2);font-size:10px;font-weight:600;letter-spacing:.04em;font-variant-numeric:tabular-nums}
|
|
500
|
+
.dsh-np-archived-chip{flex:none;padding:0 6px;border-radius:5px;background:var(--dsw-alias-interactive-bg-hover,rgb(38 49 72/.06));color:var(--dsw-alias-label-tertiary,#6b7280);font-size:10px;line-height:16px}
|
|
501
|
+
.dsh-np-segbtn{flex:none;display:inline-flex;align-items:center;justify-content:center;width:24px;height:24px;padding:0;border:0;border-radius:6px;background:transparent;color:var(--dsw-alias-label-tertiary,#6b7280);cursor:pointer;transition:background .12s,color .12s,opacity .12s;opacity:.8}
|
|
502
|
+
.dsh-np-segbtn:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover,rgb(38 49 72/.06));color:var(--dsw-alias-label-primary,#1f2430);opacity:1}
|
|
503
|
+
.dsh-np-segbtn[data-danger=true]:hover:not(:disabled){color:var(--dsw-alias-state-error-primary,#e25555);background:var(--dsw-alias-interactive-bg-hover-danger,rgb(236 19 19/.05))}
|
|
504
|
+
.dsh-np-segbtn:disabled{opacity:.3;cursor:default}
|
|
505
|
+
.dsh-np-seg-text{display:block;width:100%;box-sizing:border-box;padding:2px 10px 8px;border:0;outline:0;resize:none;background:transparent;color:inherit;font-family:${FONT_STACK};font-size:13px;line-height:1.6;min-height:26px;max-height:240px;overflow-y:auto;white-space:pre-wrap;overflow-wrap:anywhere;tab-size:2}
|
|
506
|
+
.dsh-np-seg-text::placeholder{color:var(--dsw-alias-label-caption,#9aa3b2)}
|
|
507
|
+
.dsh-np-seg-text:disabled{opacity:.6}
|
|
508
|
+
.dsh-np-seg-archived-text{padding:2px 10px 9px;color:var(--dsw-alias-label-secondary,#4b5563);font-size:13px;line-height:1.6;white-space:pre-wrap;overflow-wrap:anywhere}
|
|
509
|
+
.dsh-np-empty{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:8px;flex:none;padding:22px 12px;text-align:center;color:var(--dsw-alias-label-tertiary,#6b7280);font-size:12px}
|
|
510
|
+
.dsh-np-empty-add{display:inline-flex;align-items:center;gap:4px;padding:4px 10px;border:1px solid var(--dsw-alias-border-l2,rgb(15 23 42/.12));border-radius:7px;background:var(--np-surface-2);color:var(--dsw-alias-label-secondary,#4b5563);font:inherit;font-size:12px;cursor:pointer}
|
|
511
|
+
.dsh-np-empty-add:hover{color:var(--dsw-alias-label-primary,#1f2430);background:var(--dsw-alias-interactive-bg-hover,rgb(38 49 72/.06))}
|
|
512
|
+
.dsh-np-empty-small{padding:8px 2px;color:var(--dsw-alias-label-tertiary,#6b7280);font-size:11px}
|
|
513
|
+
.dsh-np-archive{display:grid;grid-template-rows:0fr;flex:none;opacity:0;visibility:hidden;overflow:hidden;transition:grid-template-rows .22s ease,opacity .18s ease,visibility 0s linear .22s}
|
|
514
|
+
.dsh-np-archive[data-open=true]{grid-template-rows:1fr;opacity:1;visibility:visible;transition:grid-template-rows .22s ease,opacity .18s ease,visibility 0s linear 0s}
|
|
515
|
+
.dsh-np-archive-inner{display:flex;flex-direction:column;gap:8px;min-height:0;overflow:hidden}
|
|
516
|
+
.dsh-np-archive-head{display:flex;align-items:center;gap:5px;padding:7px 2px 0;margin-top:2px;border-top:1px dashed var(--dsw-alias-border-l2,rgb(15 23 42/.12));color:var(--dsw-alias-label-tertiary,#6b7280);font-size:11px}
|
|
517
|
+
.dsh-np-archive-label{font-weight:600;letter-spacing:.02em}
|
|
518
|
+
.dsh-np-archive-list{display:flex;flex-direction:column;gap:8px;min-height:0}
|
|
519
|
+
.dsh-np-link{padding:0;border:0;background:none;color:var(--dsw-alias-link,#4176e6);font:inherit;font-size:11px;cursor:pointer;white-space:nowrap}
|
|
520
|
+
.dsh-np-link:hover{text-decoration:underline}
|
|
521
|
+
.dsh-np-link[data-danger=true]{color:var(--dsw-alias-state-error-primary,#e25555)}
|
|
522
|
+
.dsh-np-foot{display:flex;align-items:center;gap:8px;flex:none;padding:6px 18px 8px 2px;color:var(--dsw-alias-label-tertiary,#6b7280);font-size:10.5px;line-height:14px}
|
|
523
|
+
.dsh-np-status{flex:none;max-width:42%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
524
|
+
.dsh-np-status[data-state=error]{color:var(--dsw-alias-state-error-primary,#d94a4a)}
|
|
525
|
+
.dsh-np-retry{flex:none;padding:0;border:0;background:none;color:var(--dsw-alias-link,#4176e6);font:inherit;font-size:10.5px;cursor:pointer;text-decoration:underline}
|
|
526
|
+
.dsh-np-count{flex:none;font-variant-numeric:tabular-nums}
|
|
527
|
+
.dsh-np-hint{margin-left:auto;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;opacity:.85;font-size:10px}
|
|
528
|
+
.dsh-np-toast{position:absolute;left:50%;bottom:38px;transform:translateX(-50%);z-index:2;display:flex;align-items:center;gap:8px;max-width:calc(100% - 32px);padding:5px 10px;border-radius:8px;background:var(--np-surface-2);border:1px solid var(--dsw-alias-border-l2,rgb(15 23 42/.12));box-shadow:var(--dsw-elevation-lv2,0 8px 24px rgb(15 23 42/.18));color:var(--dsw-alias-label-primary,#1f2430);font-size:11.5px;white-space:nowrap;overflow:hidden;animation:dsh-np-rise .16s ease-out}
|
|
529
|
+
.dsh-np-toast-text{overflow:hidden;text-overflow:ellipsis}
|
|
530
|
+
.dsh-np-toast-action{flex:none;padding:0;border:0;background:none;color:var(--dsw-alias-link,#4176e6);font:inherit;font-size:11.5px;font-weight:600;cursor:pointer;text-decoration:underline}
|
|
531
|
+
.dsh-np-resize{position:absolute;right:0;bottom:0;z-index:3;width:18px;height:18px;cursor:nwse-resize;color:var(--dsw-alias-label-caption,#9aa3b2);opacity:.65;touch-action:none}
|
|
532
|
+
.dsh-np-resize:hover{opacity:1}
|
|
533
|
+
.dsh-np-resize::before,.dsh-np-resize::after{content:"";position:absolute;right:3px;bottom:3px;width:8px;height:8px;border-right:1.5px solid currentColor;border-bottom:1.5px solid currentColor;border-radius:0 0 2px 0}
|
|
534
|
+
.dsh-np-resize::after{right:7px;bottom:7px;width:4px;height:4px;opacity:.7}
|
|
535
|
+
.dsh-np-restore{position:fixed;right:16px;bottom:16px;z-index:30;display:inline-flex;align-items:center;justify-content:center;width:34px;height:34px;padding:0;border:1px solid var(--dsw-alias-border-l2,rgb(15 23 42/.12));border-radius:10px;background:var(--dsw-alias-bg-layer-2,#fff);color:var(--dsw-alias-label-secondary,#4b5563);box-shadow:var(--dsw-elevation-lv2,0 8px 24px rgb(15 23 42/.16));cursor:pointer;opacity:0;visibility:hidden;pointer-events:none;transform:translateY(6px) scale(.9);transition:opacity .18s ease,transform .18s ease,visibility 0s linear .18s}
|
|
536
|
+
.dsh-np-restore[data-visible=true]{opacity:.9;visibility:visible;pointer-events:auto;transform:none;transition:opacity .18s ease,transform .18s ease,visibility 0s linear 0s}
|
|
537
|
+
.dsh-np-restore[data-visible=true]:hover{opacity:1;transform:translateY(-1px)}
|
|
538
|
+
@keyframes dsh-np-pulse{0%,100%{opacity:1}50%{opacity:.35}}
|
|
539
|
+
@keyframes dsh-np-rise{from{opacity:0;transform:translate(-50%,4px)}to{opacity:1;transform:translate(-50%,0)}}
|
|
540
|
+
@keyframes dsh-np-item-in{from{opacity:0;transform:translateY(-3px)}to{opacity:1;transform:none}}
|
|
541
|
+
body[data-ds-dark-theme] .dsh-np-card{--np-accent-fg:#0f1115;box-shadow:0 18px 48px rgb(0 0 0/.55),0 0 0 .5px rgb(255 255 255/.07)}
|
|
542
|
+
body[data-ds-dark-theme] .dsh-np-seg[data-selected=true]{background:color-mix(in srgb,var(--np-accent) 14%,var(--dsw-alias-bg-layer-2,#2c2c2e))}
|
|
543
|
+
body[data-ds-dark-theme] .dsh-np-btn[data-accent=true]:hover:not(:disabled){background:color-mix(in srgb,var(--np-accent) 84%,#fff)}
|
|
544
|
+
body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
|
|
545
|
+
@media (prefers-reduced-motion:reduce){.dsh-np-card,.dsh-np-body,.dsh-np-archive,.dsh-np-restore,.dsh-np-seg,.dsh-np-toast{transition:none!important;animation:none!important}.dsh-np-dot[data-state=saving]{animation:none}}`;
|
|
546
|
+
|
|
547
|
+
// ------------------------------------------------------------------
|
|
548
|
+
// Helpers
|
|
549
|
+
// ------------------------------------------------------------------
|
|
550
|
+
let segmentSequence = 0;
|
|
551
|
+
|
|
552
|
+
function newSegmentId() {
|
|
553
|
+
const cryptoObject = typeof globalThis !== 'undefined' ? globalThis.crypto : undefined;
|
|
554
|
+
if (cryptoObject !== undefined && typeof cryptoObject.randomUUID === 'function') {
|
|
555
|
+
try {
|
|
556
|
+
return cryptoObject.randomUUID();
|
|
557
|
+
} catch {
|
|
558
|
+
/* fall through */
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
segmentSequence += 1;
|
|
562
|
+
return `seg-${Date.now().toString(36)}-${segmentSequence.toString(36)}`;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
function newSegment(text) {
|
|
566
|
+
return { id: newSegmentId(), text, archived: false, archivedAt: null };
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
function serializeSegments(segments) {
|
|
570
|
+
return segments.map((segment) => ({
|
|
571
|
+
id: segment.id,
|
|
572
|
+
text: segment.text,
|
|
573
|
+
...(segment.archived === true ? { archived: true } : {}),
|
|
574
|
+
...(segment.archived === true && typeof segment.archivedAt === 'string' ? { archivedAt: segment.archivedAt } : {}),
|
|
575
|
+
}));
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
function normalizeServerSegments(data) {
|
|
579
|
+
if (Array.isArray(data?.segments)) {
|
|
580
|
+
const seen = new Set();
|
|
581
|
+
const out = [];
|
|
582
|
+
for (const item of data.segments) {
|
|
583
|
+
if (item === null || typeof item !== 'object' || typeof item.text !== 'string') continue;
|
|
584
|
+
let id = typeof item.id === 'string' && item.id !== '' ? item.id : newSegmentId();
|
|
585
|
+
while (seen.has(id)) id = newSegmentId();
|
|
586
|
+
seen.add(id);
|
|
587
|
+
const archived = item.archived === true;
|
|
588
|
+
out.push({
|
|
589
|
+
id,
|
|
590
|
+
text: item.text,
|
|
591
|
+
archived,
|
|
592
|
+
archivedAt: archived && typeof item.archivedAt === 'string' ? item.archivedAt : null,
|
|
593
|
+
});
|
|
594
|
+
}
|
|
595
|
+
return out;
|
|
596
|
+
}
|
|
597
|
+
const text = typeof data?.text === 'string' ? data.text : '';
|
|
598
|
+
return text === '' ? [] : [newSegment(text)];
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
function mergedText(segments) {
|
|
602
|
+
return segments
|
|
603
|
+
.map((segment) => (segment !== null && segment !== undefined && typeof segment.text === 'string' ? segment.text : ''))
|
|
604
|
+
.filter((text) => text.trim() !== '')
|
|
605
|
+
.join('\n\n');
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
function autoGrow(element) {
|
|
609
|
+
if (element === null || element === undefined || element.style === undefined) return;
|
|
610
|
+
try {
|
|
611
|
+
element.style.height = 'auto';
|
|
612
|
+
const height = Number(element.scrollHeight);
|
|
613
|
+
if (Number.isFinite(height) && height > 0) {
|
|
614
|
+
element.style.height = `${Math.min(240, Math.max(26, height))}px`;
|
|
615
|
+
}
|
|
616
|
+
} catch {
|
|
617
|
+
/* A non-layout shim (tests) may not expose scrollHeight. */
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
function workspaceName(root) {
|
|
622
|
+
const parts = String(root ?? '').split(/[\\/]+/u).filter(Boolean);
|
|
623
|
+
return parts.length === 0 ? String(root ?? '') : parts[parts.length - 1];
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
function readStoredUi() {
|
|
627
|
+
const base = {
|
|
628
|
+
x: null,
|
|
629
|
+
y: null,
|
|
630
|
+
width: DEFAULT_W,
|
|
631
|
+
height: DEFAULT_H,
|
|
632
|
+
minimized: false,
|
|
633
|
+
hidden: false,
|
|
634
|
+
showArchive: false,
|
|
635
|
+
};
|
|
636
|
+
try {
|
|
637
|
+
const raw = window.localStorage?.getItem(UI_KEY);
|
|
638
|
+
if (typeof raw !== 'string' || raw === '') return base;
|
|
639
|
+
const parsed = JSON.parse(raw);
|
|
640
|
+
if (parsed === null || typeof parsed !== 'object') return base;
|
|
641
|
+
return {
|
|
642
|
+
x: Number.isFinite(parsed.x) ? parsed.x : null,
|
|
643
|
+
y: Number.isFinite(parsed.y) ? parsed.y : null,
|
|
644
|
+
width: Number.isFinite(parsed.width) ? parsed.width : DEFAULT_W,
|
|
645
|
+
height: Number.isFinite(parsed.height) ? parsed.height : DEFAULT_H,
|
|
646
|
+
minimized: parsed.minimized === true,
|
|
647
|
+
hidden: parsed.hidden === true,
|
|
648
|
+
showArchive: parsed.showArchive === true,
|
|
649
|
+
};
|
|
650
|
+
} catch {
|
|
651
|
+
return base;
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
function clampUi(ui) {
|
|
656
|
+
const vw = (typeof window !== 'undefined' && window.innerWidth) || 1280;
|
|
657
|
+
const vh = (typeof window !== 'undefined' && window.innerHeight) || 800;
|
|
658
|
+
const widthLimit = Math.max(240, vw - 16);
|
|
659
|
+
const heightLimit = Math.max(180, vh - 16);
|
|
660
|
+
const width = Math.min(Math.max(MIN_W, Number(ui.width) || DEFAULT_W), widthLimit);
|
|
661
|
+
const height = Math.min(Math.max(MIN_H, Number(ui.height) || DEFAULT_H), heightLimit);
|
|
662
|
+
const displayHeight = ui.minimized ? HEAD_H : height;
|
|
663
|
+
const x = ui.x === null || ui.x === undefined
|
|
664
|
+
? vw - width - 24
|
|
665
|
+
: Math.min(Math.max(8, ui.x), Math.max(8, vw - width - 8));
|
|
666
|
+
const y = ui.y === null || ui.y === undefined
|
|
667
|
+
? vh - displayHeight - 24
|
|
668
|
+
: Math.min(Math.max(8, ui.y), Math.max(8, vh - displayHeight - 8));
|
|
669
|
+
return { ...ui, x, y, width, height };
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
async function readResponse(response) {
|
|
673
|
+
let data = {};
|
|
674
|
+
try {
|
|
675
|
+
data = await response.json();
|
|
676
|
+
} catch {
|
|
677
|
+
data = {};
|
|
678
|
+
}
|
|
679
|
+
if (!response.ok || data?.ok === false) {
|
|
680
|
+
throw new Error(data?.error || `HTTP ${response.status}`);
|
|
681
|
+
}
|
|
682
|
+
return data;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
async function fetchNote(root, sessionId) {
|
|
686
|
+
const query = new URLSearchParams({ root, sessionId });
|
|
687
|
+
const response = await fetch(`${API}/note?${query.toString()}`, {
|
|
688
|
+
method: 'GET',
|
|
689
|
+
cache: 'no-store',
|
|
690
|
+
});
|
|
691
|
+
return readResponse(response);
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
async function saveNote(root, sessionId, segments) {
|
|
695
|
+
const response = await fetch(`${API}/note`, {
|
|
696
|
+
method: 'PUT',
|
|
697
|
+
headers: { 'Content-Type': 'application/json' },
|
|
698
|
+
body: JSON.stringify({ root, sessionId, segments }),
|
|
699
|
+
});
|
|
700
|
+
return readResponse(response);
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
async function copyText(value) {
|
|
704
|
+
try {
|
|
705
|
+
if (typeof navigator !== 'undefined' && navigator.clipboard?.writeText) {
|
|
706
|
+
await navigator.clipboard.writeText(value);
|
|
707
|
+
return true;
|
|
708
|
+
}
|
|
709
|
+
} catch {
|
|
710
|
+
/* fall through to execCommand */
|
|
711
|
+
}
|
|
712
|
+
try {
|
|
713
|
+
const area = document.createElement('textarea');
|
|
714
|
+
area.value = value;
|
|
715
|
+
area.setAttribute('readonly', '');
|
|
716
|
+
area.style.position = 'fixed';
|
|
717
|
+
area.style.left = '-9999px';
|
|
718
|
+
area.style.top = '0';
|
|
719
|
+
document.body.appendChild(area);
|
|
720
|
+
area.select();
|
|
721
|
+
const ok = typeof document.execCommand === 'function' ? document.execCommand('copy') : false;
|
|
722
|
+
area.remove();
|
|
723
|
+
return ok;
|
|
724
|
+
} catch {
|
|
725
|
+
return false;
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
function clockLabel(value) {
|
|
730
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) return '';
|
|
731
|
+
try {
|
|
732
|
+
return new Date(value).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
|
733
|
+
} catch {
|
|
734
|
+
return '';
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
function presentConversation() {
|
|
739
|
+
try {
|
|
740
|
+
if (typeof document === 'undefined') return false;
|
|
741
|
+
const node = document.querySelector('[data-conversation-content]');
|
|
742
|
+
if (node === null || node === undefined) return false;
|
|
743
|
+
if (typeof node.getBoundingClientRect === 'function') {
|
|
744
|
+
const rect = node.getBoundingClientRect();
|
|
745
|
+
return rect.width > 0 && rect.height > 0;
|
|
746
|
+
}
|
|
747
|
+
return true;
|
|
748
|
+
} catch {
|
|
749
|
+
return false;
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
// ------------------------------------------------------------------
|
|
754
|
+
// Overlay
|
|
755
|
+
// ------------------------------------------------------------------
|
|
756
|
+
|
|
757
|
+
/**
|
|
758
|
+
* @param {{ ctx: any, t: (key: string, params?: Record<string, unknown>) => string }} props
|
|
759
|
+
*/
|
|
760
|
+
function NotepadOverlay({ ctx, t }) {
|
|
761
|
+
const [binding, setBinding] = useState(null);
|
|
762
|
+
const [present, setPresent] = useState(presentConversation);
|
|
763
|
+
const [segments, setSegments] = useState([]);
|
|
764
|
+
const [selected, setSelected] = useState(() => new Set());
|
|
765
|
+
const [status, setStatus] = useState('idle');
|
|
766
|
+
const [error, setError] = useState('');
|
|
767
|
+
const [savedAt, setSavedAt] = useState(null);
|
|
768
|
+
const [toast, setToast] = useState(null);
|
|
769
|
+
const [ui, setUi] = useState(readStoredUi);
|
|
770
|
+
const [sending, setSending] = useState(false);
|
|
771
|
+
const [resizing, setResizing] = useState(false);
|
|
772
|
+
|
|
773
|
+
const bindingRef = useRef(null);
|
|
774
|
+
const rootSessionRef = useRef('');
|
|
775
|
+
const segmentsRef = useRef([]);
|
|
776
|
+
const selectedRef = useRef(selected);
|
|
777
|
+
const rootRef = useRef('');
|
|
778
|
+
const versionRef = useRef(null);
|
|
779
|
+
const dirtyRef = useRef(false);
|
|
780
|
+
const timerRef = useRef(0);
|
|
781
|
+
const toastTimerRef = useRef(0);
|
|
782
|
+
const seqRef = useRef(0);
|
|
783
|
+
const saveNowRef = useRef(null);
|
|
784
|
+
const reloadRef = useRef(null);
|
|
785
|
+
const loadFailedRef = useRef(false);
|
|
786
|
+
const loadSeqRef = useRef(0);
|
|
787
|
+
const inputActionsRef = useRef(null);
|
|
788
|
+
const dragRef = useRef(null);
|
|
789
|
+
const resizeRef = useRef(null);
|
|
790
|
+
const sendingRef = useRef(false);
|
|
791
|
+
const textareaRefs = useRef(new Map());
|
|
792
|
+
const listRef = useRef(null);
|
|
793
|
+
const pendingFocusRef = useRef(null);
|
|
794
|
+
const undoRef = useRef(null);
|
|
795
|
+
|
|
796
|
+
const setSelection = (next) => {
|
|
797
|
+
selectedRef.current = next;
|
|
798
|
+
setSelected(next);
|
|
799
|
+
};
|
|
800
|
+
|
|
801
|
+
const pruneSelectionFor = (nextSegments) => {
|
|
802
|
+
if (selectedRef.current.size === 0) return;
|
|
803
|
+
const activeIds = new Set(nextSegments.filter((segment) => segment.archived !== true).map((segment) => segment.id));
|
|
804
|
+
const pruned = new Set();
|
|
805
|
+
let changed = false;
|
|
806
|
+
for (const id of selectedRef.current) {
|
|
807
|
+
if (activeIds.has(id)) pruned.add(id);
|
|
808
|
+
else changed = true;
|
|
809
|
+
}
|
|
810
|
+
if (changed) setSelection(pruned);
|
|
811
|
+
};
|
|
812
|
+
|
|
813
|
+
// Session + workspace -------------------------------------------------
|
|
814
|
+
useEffect(() => {
|
|
815
|
+
let disposed = false;
|
|
816
|
+
const uiSession = ctx.get('uiSession');
|
|
817
|
+
const sessions = ctx.get('sessions');
|
|
818
|
+
const current = uiSession?.adapter?.current;
|
|
819
|
+
if (current === undefined || sessions === undefined) {
|
|
820
|
+
setBinding(null);
|
|
821
|
+
return undefined;
|
|
822
|
+
}
|
|
823
|
+
const sync = () => {
|
|
824
|
+
if (disposed) return;
|
|
825
|
+
const snapshot = current.getSnapshot();
|
|
826
|
+
const id = typeof snapshot?.key === 'string' ? snapshot.key : undefined;
|
|
827
|
+
const row = id !== undefined ? sessions.list.getSnapshot()?.byId?.[id] : undefined;
|
|
828
|
+
const cwd = typeof row?.cwd === 'string' && row.cwd !== '' ? row.cwd : undefined;
|
|
829
|
+
inputActionsRef.current = snapshot?.props?.inputActions ?? null;
|
|
830
|
+
const next = id !== undefined && cwd !== undefined ? { id, cwd } : null;
|
|
831
|
+
bindingRef.current = next;
|
|
832
|
+
setBinding((previous) =>
|
|
833
|
+
previous !== null && next !== null && previous.id === next.id && previous.cwd === next.cwd
|
|
834
|
+
? previous
|
|
835
|
+
: next);
|
|
836
|
+
};
|
|
837
|
+
const unsubscribeCurrent = current.subscribe(sync);
|
|
838
|
+
const unsubscribeSessions = sessions.list.subscribe(sync);
|
|
839
|
+
sync();
|
|
840
|
+
return () => {
|
|
841
|
+
disposed = true;
|
|
842
|
+
unsubscribeCurrent?.();
|
|
843
|
+
unsubscribeSessions?.();
|
|
844
|
+
};
|
|
845
|
+
}, []);
|
|
846
|
+
|
|
847
|
+
// Only render over the conversation view, not over settings pages ----
|
|
848
|
+
useEffect(() => {
|
|
849
|
+
if (typeof document === 'undefined') return undefined;
|
|
850
|
+
const check = () => {
|
|
851
|
+
const visible = presentConversation();
|
|
852
|
+
setPresent((previous) => (previous === visible ? previous : visible));
|
|
853
|
+
};
|
|
854
|
+
check();
|
|
855
|
+
const timer = setInterval(check, 700);
|
|
856
|
+
return () => clearInterval(timer);
|
|
857
|
+
}, []);
|
|
858
|
+
|
|
859
|
+
// Position + size ----------------------------------------------------
|
|
860
|
+
useEffect(() => {
|
|
861
|
+
setUi((previous) => clampUi(previous));
|
|
862
|
+
const onResize = () => setUi((previous) => clampUi(previous));
|
|
863
|
+
window.addEventListener('resize', onResize);
|
|
864
|
+
return () => window.removeEventListener('resize', onResize);
|
|
865
|
+
}, []);
|
|
866
|
+
|
|
867
|
+
useEffect(() => {
|
|
868
|
+
try {
|
|
869
|
+
window.localStorage?.setItem(UI_KEY, JSON.stringify(ui));
|
|
870
|
+
} catch {
|
|
871
|
+
/* Storage may be unavailable; the widget still works per page. */
|
|
872
|
+
}
|
|
873
|
+
}, [ui]);
|
|
874
|
+
|
|
875
|
+
// Save plumbing -------------------------------------------------------
|
|
876
|
+
const clearTimer = () => {
|
|
877
|
+
if (timerRef.current) {
|
|
878
|
+
clearTimeout(timerRef.current);
|
|
879
|
+
timerRef.current = 0;
|
|
880
|
+
}
|
|
881
|
+
};
|
|
882
|
+
|
|
883
|
+
const showToast = (message, action) => {
|
|
884
|
+
setToast({ id: Date.now(), message, action: action ?? null });
|
|
885
|
+
if (toastTimerRef.current) clearTimeout(toastTimerRef.current);
|
|
886
|
+
toastTimerRef.current = setTimeout(() => setToast(null), action === undefined || action === null ? 1800 : 5000);
|
|
887
|
+
};
|
|
888
|
+
|
|
889
|
+
const scheduleSave = () => {
|
|
890
|
+
clearTimer();
|
|
891
|
+
timerRef.current = setTimeout(() => {
|
|
892
|
+
timerRef.current = 0;
|
|
893
|
+
saveNowRef.current?.();
|
|
894
|
+
}, SAVE_DELAY);
|
|
895
|
+
};
|
|
896
|
+
|
|
897
|
+
const saveNow = async (options = {}) => {
|
|
898
|
+
clearTimer();
|
|
899
|
+
const root = rootRef.current;
|
|
900
|
+
const sessionId = rootSessionRef.current;
|
|
901
|
+
if (root === '' || sessionId === '') return;
|
|
902
|
+
if (!dirtyRef.current && options.force !== true) return;
|
|
903
|
+
const body = serializeSegments(segmentsRef.current);
|
|
904
|
+
const signature = JSON.stringify(body);
|
|
905
|
+
const seq = ++seqRef.current;
|
|
906
|
+
dirtyRef.current = false;
|
|
907
|
+
setStatus('saving');
|
|
908
|
+
setError('');
|
|
909
|
+
try {
|
|
910
|
+
const data = await saveNote(root, sessionId, body);
|
|
911
|
+
if (seq !== seqRef.current || rootRef.current !== root) return;
|
|
912
|
+
if (typeof data.mtimeMs === 'number') versionRef.current = data.mtimeMs;
|
|
913
|
+
setSavedAt(typeof data.mtimeMs === 'number' ? data.mtimeMs : Date.now());
|
|
914
|
+
if (JSON.stringify(serializeSegments(segmentsRef.current)) === signature) {
|
|
915
|
+
dirtyRef.current = false;
|
|
916
|
+
setStatus('saved');
|
|
917
|
+
} else {
|
|
918
|
+
dirtyRef.current = true;
|
|
919
|
+
setStatus('dirty');
|
|
920
|
+
scheduleSave();
|
|
921
|
+
}
|
|
922
|
+
} catch (reason) {
|
|
923
|
+
if (seq !== seqRef.current || rootRef.current !== root) return;
|
|
924
|
+
dirtyRef.current = true;
|
|
925
|
+
setStatus('error');
|
|
926
|
+
setError(String(reason?.message || reason));
|
|
927
|
+
}
|
|
928
|
+
};
|
|
929
|
+
saveNowRef.current = saveNow;
|
|
930
|
+
|
|
931
|
+
const commitSegments = (next, options = {}) => {
|
|
932
|
+
segmentsRef.current = next;
|
|
933
|
+
setSegments(next);
|
|
934
|
+
pruneSelectionFor(next);
|
|
935
|
+
if (options.focus !== undefined) pendingFocusRef.current = options.focus;
|
|
936
|
+
dirtyRef.current = true;
|
|
937
|
+
setStatus('dirty');
|
|
938
|
+
setError('');
|
|
939
|
+
scheduleSave();
|
|
940
|
+
};
|
|
941
|
+
|
|
942
|
+
// Load on workspace/session change ------------------------------------
|
|
943
|
+
useEffect(() => {
|
|
944
|
+
if (binding === null) {
|
|
945
|
+
rootRef.current = '';
|
|
946
|
+
rootSessionRef.current = '';
|
|
947
|
+
segmentsRef.current = [];
|
|
948
|
+
setSegments([]);
|
|
949
|
+
setSelection(new Set());
|
|
950
|
+
setStatus('idle');
|
|
951
|
+
setError('');
|
|
952
|
+
return undefined;
|
|
953
|
+
}
|
|
954
|
+
if (dirtyRef.current && rootRef.current !== '' && rootRef.current !== binding.cwd) {
|
|
955
|
+
saveNowRef.current?.({ force: true });
|
|
956
|
+
}
|
|
957
|
+
clearTimer();
|
|
958
|
+
const root = binding.cwd;
|
|
959
|
+
rootRef.current = root;
|
|
960
|
+
rootSessionRef.current = binding.id;
|
|
961
|
+
let cancelled = false;
|
|
962
|
+
loadFailedRef.current = false;
|
|
963
|
+
segmentsRef.current = [];
|
|
964
|
+
setSegments([]);
|
|
965
|
+
setSelection(new Set());
|
|
966
|
+
setStatus('loading');
|
|
967
|
+
setError('');
|
|
968
|
+
const load = () => {
|
|
969
|
+
const mySeq = ++loadSeqRef.current;
|
|
970
|
+
loadFailedRef.current = false;
|
|
971
|
+
setStatus('loading');
|
|
972
|
+
setError('');
|
|
973
|
+
fetchNote(root, binding.id)
|
|
974
|
+
.then((data) => {
|
|
975
|
+
if (cancelled || rootRef.current !== root || mySeq !== loadSeqRef.current) return;
|
|
976
|
+
let value = normalizeServerSegments(data);
|
|
977
|
+
if (value.length === 0) value = [newSegment('')];
|
|
978
|
+
segmentsRef.current = value;
|
|
979
|
+
setSegments(value);
|
|
980
|
+
setSelection(new Set());
|
|
981
|
+
versionRef.current = typeof data.mtimeMs === 'number' ? data.mtimeMs : null;
|
|
982
|
+
setSavedAt(typeof data.mtimeMs === 'number' ? data.mtimeMs : null);
|
|
983
|
+
dirtyRef.current = false;
|
|
984
|
+
setStatus('saved');
|
|
985
|
+
})
|
|
986
|
+
.catch((reason) => {
|
|
987
|
+
if (cancelled || rootRef.current !== root || mySeq !== loadSeqRef.current) return;
|
|
988
|
+
loadFailedRef.current = true;
|
|
989
|
+
setStatus('error');
|
|
990
|
+
setError(String(reason?.message || reason));
|
|
991
|
+
});
|
|
992
|
+
};
|
|
993
|
+
reloadRef.current = load;
|
|
994
|
+
load();
|
|
995
|
+
return () => {
|
|
996
|
+
cancelled = true;
|
|
997
|
+
if (reloadRef.current === load) reloadRef.current = null;
|
|
998
|
+
};
|
|
999
|
+
}, [binding === null ? '' : binding.id, binding === null ? '' : binding.cwd]);
|
|
1000
|
+
|
|
1001
|
+
// Focus a just-created or restored card ------------------------------
|
|
1002
|
+
useEffect(() => {
|
|
1003
|
+
const pending = pendingFocusRef.current;
|
|
1004
|
+
if (pending === null || pending === undefined) return;
|
|
1005
|
+
pendingFocusRef.current = null;
|
|
1006
|
+
const element = textareaRefs.current.get(pending.id);
|
|
1007
|
+
if (element === undefined || element === null) return;
|
|
1008
|
+
try {
|
|
1009
|
+
element.focus?.({ preventScroll: true });
|
|
1010
|
+
} catch {
|
|
1011
|
+
try {
|
|
1012
|
+
element.focus?.();
|
|
1013
|
+
} catch {
|
|
1014
|
+
/* ignore */
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
try {
|
|
1018
|
+
const value = typeof element.value === 'string' ? element.value : '';
|
|
1019
|
+
const caret = Math.max(0, Math.min(Number(pending.caret) || 0, value.length));
|
|
1020
|
+
if (typeof element.setSelectionRange === 'function') element.setSelectionRange(caret, caret);
|
|
1021
|
+
} catch {
|
|
1022
|
+
/* ignore */
|
|
1023
|
+
}
|
|
1024
|
+
try {
|
|
1025
|
+
element.scrollIntoView?.({ block: 'nearest' });
|
|
1026
|
+
} catch {
|
|
1027
|
+
/* ignore */
|
|
1028
|
+
}
|
|
1029
|
+
autoGrow(element);
|
|
1030
|
+
}, [segments]);
|
|
1031
|
+
|
|
1032
|
+
// Keyboard ------------------------------------------------------------
|
|
1033
|
+
useEffect(() => {
|
|
1034
|
+
const onKeyDown = (event) => {
|
|
1035
|
+
const chord = event.ctrlKey || event.metaKey;
|
|
1036
|
+
if (!chord || !event.shiftKey) return;
|
|
1037
|
+
if (event.key !== 'N' && event.key !== 'n') return;
|
|
1038
|
+
event.preventDefault();
|
|
1039
|
+
setUi((previous) => clampUi({ ...previous, hidden: !previous.hidden }));
|
|
1040
|
+
};
|
|
1041
|
+
window.addEventListener('keydown', onKeyDown, true);
|
|
1042
|
+
return () => {
|
|
1043
|
+
window.removeEventListener('keydown', onKeyDown, true);
|
|
1044
|
+
clearTimer();
|
|
1045
|
+
if (toastTimerRef.current) clearTimeout(toastTimerRef.current);
|
|
1046
|
+
};
|
|
1047
|
+
}, []);
|
|
1048
|
+
|
|
1049
|
+
// Flush on page hide / reload -----------------------------------------
|
|
1050
|
+
useEffect(() => {
|
|
1051
|
+
const flush = () => {
|
|
1052
|
+
if (dirtyRef.current) saveNowRef.current?.({ force: true });
|
|
1053
|
+
};
|
|
1054
|
+
const onVisibility = () => {
|
|
1055
|
+
if (typeof document !== 'undefined' && document.visibilityState === 'hidden') flush();
|
|
1056
|
+
};
|
|
1057
|
+
window.addEventListener('pagehide', flush);
|
|
1058
|
+
if (typeof document !== 'undefined') {
|
|
1059
|
+
document.addEventListener('visibilitychange', onVisibility);
|
|
1060
|
+
}
|
|
1061
|
+
return () => {
|
|
1062
|
+
flush();
|
|
1063
|
+
window.removeEventListener('pagehide', flush);
|
|
1064
|
+
if (typeof document !== 'undefined') {
|
|
1065
|
+
document.removeEventListener('visibilitychange', onVisibility);
|
|
1066
|
+
}
|
|
1067
|
+
};
|
|
1068
|
+
}, []);
|
|
1069
|
+
|
|
1070
|
+
// Adopt external edits when the window regains focus ------------------
|
|
1071
|
+
useEffect(() => {
|
|
1072
|
+
if (typeof window === 'undefined') return undefined;
|
|
1073
|
+
const refresh = () => {
|
|
1074
|
+
const root = rootRef.current;
|
|
1075
|
+
const sessionId = rootSessionRef.current;
|
|
1076
|
+
if (root === '' || sessionId === '' || dirtyRef.current || timerRef.current) return;
|
|
1077
|
+
const active = typeof document !== 'undefined' ? document.activeElement : null;
|
|
1078
|
+
if (active !== null && active !== undefined && typeof active.closest === 'function'
|
|
1079
|
+
&& active.closest('[data-dsh-notepad="true"]') !== null) return;
|
|
1080
|
+
fetchNote(root, sessionId)
|
|
1081
|
+
.then((data) => {
|
|
1082
|
+
if (rootRef.current !== root || dirtyRef.current) return;
|
|
1083
|
+
if (typeof data.mtimeMs === 'number' && data.mtimeMs === versionRef.current) return;
|
|
1084
|
+
let value = normalizeServerSegments(data);
|
|
1085
|
+
if (value.length === 0) value = [newSegment('')];
|
|
1086
|
+
segmentsRef.current = value;
|
|
1087
|
+
setSegments(value);
|
|
1088
|
+
setSelection(new Set());
|
|
1089
|
+
versionRef.current = typeof data.mtimeMs === 'number' ? data.mtimeMs : null;
|
|
1090
|
+
setSavedAt(typeof data.mtimeMs === 'number' ? data.mtimeMs : null);
|
|
1091
|
+
setStatus('saved');
|
|
1092
|
+
})
|
|
1093
|
+
.catch(() => {});
|
|
1094
|
+
};
|
|
1095
|
+
const onVisibility = () => {
|
|
1096
|
+
if (typeof document !== 'undefined' && document.visibilityState === 'visible') refresh();
|
|
1097
|
+
};
|
|
1098
|
+
window.addEventListener('focus', refresh);
|
|
1099
|
+
if (typeof document !== 'undefined') {
|
|
1100
|
+
document.addEventListener('visibilitychange', onVisibility);
|
|
1101
|
+
}
|
|
1102
|
+
return () => {
|
|
1103
|
+
window.removeEventListener('focus', refresh);
|
|
1104
|
+
if (typeof document !== 'undefined') {
|
|
1105
|
+
document.removeEventListener('visibilitychange', onVisibility);
|
|
1106
|
+
}
|
|
1107
|
+
};
|
|
1108
|
+
}, []);
|
|
1109
|
+
|
|
1110
|
+
// Segment mutations ---------------------------------------------------
|
|
1111
|
+
const updateSegmentText = (id, value) => {
|
|
1112
|
+
const next = segmentsRef.current.map((segment) => (segment.id === id ? { ...segment, text: value } : segment));
|
|
1113
|
+
segmentsRef.current = next;
|
|
1114
|
+
setSegments(next);
|
|
1115
|
+
dirtyRef.current = true;
|
|
1116
|
+
setStatus('dirty');
|
|
1117
|
+
setError('');
|
|
1118
|
+
scheduleSave();
|
|
1119
|
+
};
|
|
1120
|
+
|
|
1121
|
+
const addSegment = () => {
|
|
1122
|
+
if (segmentsRef.current.length >= MAX_SEGMENTS) {
|
|
1123
|
+
showToast(t('tooManySegments', { n: MAX_SEGMENTS }));
|
|
1124
|
+
return;
|
|
1125
|
+
}
|
|
1126
|
+
const segment = newSegment('');
|
|
1127
|
+
commitSegments([...segmentsRef.current, segment], { focus: { id: segment.id, caret: 0 } });
|
|
1128
|
+
};
|
|
1129
|
+
|
|
1130
|
+
const splitSegment = (id, element) => {
|
|
1131
|
+
const current = segmentsRef.current.find((segment) => segment.id === id);
|
|
1132
|
+
if (current === undefined) return;
|
|
1133
|
+
const start = typeof element?.selectionStart === 'number' ? element.selectionStart : current.text.length;
|
|
1134
|
+
const head = current.text.slice(0, start);
|
|
1135
|
+
const tail = current.text.slice(start);
|
|
1136
|
+
const nextSegment = newSegment(tail);
|
|
1137
|
+
const next = [];
|
|
1138
|
+
for (const segment of segmentsRef.current) {
|
|
1139
|
+
if (segment.id === id) {
|
|
1140
|
+
next.push({ ...segment, text: head });
|
|
1141
|
+
next.push(nextSegment);
|
|
1142
|
+
} else {
|
|
1143
|
+
next.push(segment);
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
commitSegments(next, { focus: { id: nextSegment.id, caret: 0 } });
|
|
1147
|
+
};
|
|
1148
|
+
|
|
1149
|
+
const archiveSegment = (id) => {
|
|
1150
|
+
const current = segmentsRef.current.find((segment) => segment.id === id && segment.archived !== true);
|
|
1151
|
+
if (current === undefined) return;
|
|
1152
|
+
const archivedAt = new Date().toISOString();
|
|
1153
|
+
const next = segmentsRef.current.map((segment) =>
|
|
1154
|
+
(segment.id === id ? { ...segment, archived: true, archivedAt } : segment));
|
|
1155
|
+
setSelection(new Set([...selectedRef.current].filter((value) => value !== id)));
|
|
1156
|
+
commitSegments(next);
|
|
1157
|
+
showToast(t('archivedToast'));
|
|
1158
|
+
};
|
|
1159
|
+
|
|
1160
|
+
const archiveSelected = () => {
|
|
1161
|
+
const ids = new Set(
|
|
1162
|
+
segmentsRef.current
|
|
1163
|
+
.filter((segment) => segment.archived !== true && selectedRef.current.has(segment.id))
|
|
1164
|
+
.map((segment) => segment.id),
|
|
1165
|
+
);
|
|
1166
|
+
if (ids.size === 0) return;
|
|
1167
|
+
const archivedAt = new Date().toISOString();
|
|
1168
|
+
const next = segmentsRef.current.map((segment) =>
|
|
1169
|
+
(ids.has(segment.id) ? { ...segment, archived: true, archivedAt } : segment));
|
|
1170
|
+
setSelection(new Set());
|
|
1171
|
+
commitSegments(next);
|
|
1172
|
+
showToast(t('archivedCount', { n: ids.size }));
|
|
1173
|
+
};
|
|
1174
|
+
|
|
1175
|
+
const restoreSegment = (id) => {
|
|
1176
|
+
const current = segmentsRef.current.find((segment) => segment.id === id && segment.archived === true);
|
|
1177
|
+
if (current === undefined) return;
|
|
1178
|
+
const next = segmentsRef.current.map((segment) =>
|
|
1179
|
+
(segment.id === id ? { ...segment, archived: false, archivedAt: null } : segment));
|
|
1180
|
+
commitSegments(next, { focus: { id, caret: current.text.length } });
|
|
1181
|
+
showToast(t('restoredToast'));
|
|
1182
|
+
};
|
|
1183
|
+
|
|
1184
|
+
const restoreAll = () => {
|
|
1185
|
+
const restored = segmentsRef.current.filter((segment) => segment.archived === true);
|
|
1186
|
+
if (restored.length === 0) return;
|
|
1187
|
+
const next = segmentsRef.current.map((segment) =>
|
|
1188
|
+
(segment.archived === true ? { ...segment, archived: false, archivedAt: null } : segment));
|
|
1189
|
+
commitSegments(next);
|
|
1190
|
+
showToast(t('restoredCount', { n: restored.length }));
|
|
1191
|
+
};
|
|
1192
|
+
|
|
1193
|
+
const undoLast = () => {
|
|
1194
|
+
const record = undoRef.current;
|
|
1195
|
+
if (record === null || record === undefined) return;
|
|
1196
|
+
undoRef.current = null;
|
|
1197
|
+
commitSegments(record.snapshot);
|
|
1198
|
+
setToast(null);
|
|
1199
|
+
};
|
|
1200
|
+
|
|
1201
|
+
const deleteArchived = (id) => {
|
|
1202
|
+
const target = segmentsRef.current.find((segment) => segment.id === id && segment.archived === true);
|
|
1203
|
+
if (target === undefined) return;
|
|
1204
|
+
undoRef.current = { snapshot: segmentsRef.current };
|
|
1205
|
+
const next = segmentsRef.current.filter((segment) => segment.id !== id);
|
|
1206
|
+
commitSegments(next);
|
|
1207
|
+
showToast(t('deletedToast'), { label: t('undo'), onClick: undoLast });
|
|
1208
|
+
};
|
|
1209
|
+
|
|
1210
|
+
const clearArchive = () => {
|
|
1211
|
+
const archived = segmentsRef.current.filter((segment) => segment.archived === true);
|
|
1212
|
+
if (archived.length === 0) return;
|
|
1213
|
+
undoRef.current = { snapshot: segmentsRef.current };
|
|
1214
|
+
const next = segmentsRef.current.filter((segment) => segment.archived !== true);
|
|
1215
|
+
commitSegments(next);
|
|
1216
|
+
showToast(t('clearedArchive', { n: archived.length }), { label: t('undo'), onClick: undoLast });
|
|
1217
|
+
};
|
|
1218
|
+
|
|
1219
|
+
// Selection -----------------------------------------------------------
|
|
1220
|
+
const toggleSelected = (id) => {
|
|
1221
|
+
const next = new Set(selectedRef.current);
|
|
1222
|
+
if (next.has(id)) next.delete(id);
|
|
1223
|
+
else next.add(id);
|
|
1224
|
+
setSelection(next);
|
|
1225
|
+
};
|
|
1226
|
+
|
|
1227
|
+
const toggleSelectAll = () => {
|
|
1228
|
+
const active = segmentsRef.current.filter((segment) => segment.archived !== true);
|
|
1229
|
+
const allSelected = active.length > 0 && active.every((segment) => selectedRef.current.has(segment.id));
|
|
1230
|
+
setSelection(allSelected ? new Set() : new Set(active.map((segment) => segment.id)));
|
|
1231
|
+
};
|
|
1232
|
+
|
|
1233
|
+
// Composer actions ----------------------------------------------------
|
|
1234
|
+
const insertValue = (value) => {
|
|
1235
|
+
const actions = inputActionsRef.current;
|
|
1236
|
+
if (actions === null || actions === undefined) return false;
|
|
1237
|
+
if (typeof actions.captureInsertion !== 'function' || typeof actions.insertText !== 'function') return false;
|
|
1238
|
+
try {
|
|
1239
|
+
const span = actions.captureInsertion();
|
|
1240
|
+
if (actions.insertText(value, span) === true) {
|
|
1241
|
+
actions.persistDraft?.();
|
|
1242
|
+
return true;
|
|
1243
|
+
}
|
|
1244
|
+
} catch {
|
|
1245
|
+
/* fall through to copy */
|
|
1246
|
+
}
|
|
1247
|
+
return false;
|
|
1248
|
+
};
|
|
1249
|
+
|
|
1250
|
+
const replaceValue = (value) => {
|
|
1251
|
+
const actions = inputActionsRef.current;
|
|
1252
|
+
if (actions === null || actions === undefined || typeof actions.setDraft !== 'function') return false;
|
|
1253
|
+
try {
|
|
1254
|
+
actions.setDraft(value);
|
|
1255
|
+
actions.persistDraft?.();
|
|
1256
|
+
return true;
|
|
1257
|
+
} catch {
|
|
1258
|
+
return false;
|
|
1259
|
+
}
|
|
1260
|
+
};
|
|
1261
|
+
|
|
1262
|
+
const insertSegments = async (list) => {
|
|
1263
|
+
const value = mergedText(list);
|
|
1264
|
+
if (value === '') {
|
|
1265
|
+
showToast(t('insertEmpty'));
|
|
1266
|
+
return;
|
|
1267
|
+
}
|
|
1268
|
+
if (insertValue(value)) {
|
|
1269
|
+
showToast(t('inserted'));
|
|
1270
|
+
return;
|
|
1271
|
+
}
|
|
1272
|
+
const copied = await copyText(value);
|
|
1273
|
+
showToast(copied ? t('insertUnavailable') : t('copyFailed'));
|
|
1274
|
+
};
|
|
1275
|
+
|
|
1276
|
+
const replaceSegments = async (list) => {
|
|
1277
|
+
const value = mergedText(list);
|
|
1278
|
+
if (value === '') {
|
|
1279
|
+
showToast(t('insertEmpty'));
|
|
1280
|
+
return;
|
|
1281
|
+
}
|
|
1282
|
+
if (replaceValue(value)) {
|
|
1283
|
+
showToast(t('replaced'));
|
|
1284
|
+
return;
|
|
1285
|
+
}
|
|
1286
|
+
const copied = await copyText(value);
|
|
1287
|
+
showToast(copied ? t('replaceUnavailable') : t('copyFailed'));
|
|
1288
|
+
};
|
|
1289
|
+
|
|
1290
|
+
/**
|
|
1291
|
+
* Send text into the current session. Prefers the documented scoped
|
|
1292
|
+
* conversation service; falls back to the session face with a local echo.
|
|
1293
|
+
* @returns {Promise<boolean>} whether a prompt was accepted
|
|
1294
|
+
*/
|
|
1295
|
+
const sendToSession = async (value) => {
|
|
1296
|
+
const sessionId = rootSessionRef.current !== '' ? rootSessionRef.current : bindingRef.current?.id;
|
|
1297
|
+
if (typeof sessionId !== 'string' || sessionId === '') return false;
|
|
1298
|
+
const sessions = ctx.get('sessions');
|
|
1299
|
+
if (sessions === undefined || sessions === null) return false;
|
|
1300
|
+
if (typeof sessions.scope === 'function') {
|
|
1301
|
+
const scoped = sessions.scope(sessionId);
|
|
1302
|
+
const conversation = scoped === undefined || scoped === null
|
|
1303
|
+
? undefined
|
|
1304
|
+
: typeof scoped.get === 'function' ? scoped.get('conversation') : scoped.conversation;
|
|
1305
|
+
if (conversation !== undefined && conversation !== null && typeof conversation.send === 'function') {
|
|
1306
|
+
await conversation.send(value);
|
|
1307
|
+
return true;
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
if (typeof sessions.binding === 'function') {
|
|
1311
|
+
const liveBinding = sessions.binding(sessionId);
|
|
1312
|
+
const session = liveBinding?.session;
|
|
1313
|
+
if (session !== undefined && session !== null && typeof session.prompt === 'function') {
|
|
1314
|
+
let echo;
|
|
1315
|
+
try {
|
|
1316
|
+
echo = typeof session.beginSubmission === 'function'
|
|
1317
|
+
? session.beginSubmission({ mode: 'queue', text: value, attachments: [] })
|
|
1318
|
+
: undefined;
|
|
1319
|
+
} catch {
|
|
1320
|
+
echo = undefined;
|
|
1321
|
+
}
|
|
1322
|
+
try {
|
|
1323
|
+
const result = await session.prompt([{ type: 'text', text: value }], 'queue', undefined, echo?.requestId);
|
|
1324
|
+
if (result !== undefined && result !== null && result.ok === false) {
|
|
1325
|
+
throw new Error(result.error?.message ?? 'prompt failed');
|
|
1326
|
+
}
|
|
1327
|
+
return true;
|
|
1328
|
+
} catch (reason) {
|
|
1329
|
+
try {
|
|
1330
|
+
echo?.abandon?.();
|
|
1331
|
+
} catch {
|
|
1332
|
+
/* ignore */
|
|
1333
|
+
}
|
|
1334
|
+
throw reason;
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
return false;
|
|
1339
|
+
};
|
|
1340
|
+
|
|
1341
|
+
const sendSegments = async (list) => {
|
|
1342
|
+
if (sendingRef.current) return;
|
|
1343
|
+
const value = mergedText(list);
|
|
1344
|
+
if (value === '') {
|
|
1345
|
+
showToast(t('insertEmpty'));
|
|
1346
|
+
return;
|
|
1347
|
+
}
|
|
1348
|
+
sendingRef.current = true;
|
|
1349
|
+
setSending(true);
|
|
1350
|
+
const targetRoot = rootRef.current;
|
|
1351
|
+
const targetSession = rootSessionRef.current;
|
|
1352
|
+
try {
|
|
1353
|
+
const accepted = await sendToSession(value);
|
|
1354
|
+
if (accepted !== true) {
|
|
1355
|
+
if (!insertValue(value)) await copyText(value);
|
|
1356
|
+
showToast(t('sendUnavailable'));
|
|
1357
|
+
return;
|
|
1358
|
+
}
|
|
1359
|
+
showToast(t('sent'));
|
|
1360
|
+
const ids = new Set(
|
|
1361
|
+
list.filter((segment) => segment.archived !== true).map((segment) => segment.id),
|
|
1362
|
+
);
|
|
1363
|
+
const stillHere = rootRef.current === targetRoot && rootSessionRef.current === targetSession;
|
|
1364
|
+
if (ids.size > 0 && stillHere) {
|
|
1365
|
+
const archivedAt = new Date().toISOString();
|
|
1366
|
+
const next = segmentsRef.current.map((segment) =>
|
|
1367
|
+
(ids.has(segment.id) ? { ...segment, archived: true, archivedAt } : segment));
|
|
1368
|
+
setSelection(new Set([...selectedRef.current].filter((id) => !ids.has(id))));
|
|
1369
|
+
commitSegments(next);
|
|
1370
|
+
}
|
|
1371
|
+
} catch (reason) {
|
|
1372
|
+
showToast(`${t('sendFailed')}: ${String(reason?.message ?? reason)}`);
|
|
1373
|
+
} finally {
|
|
1374
|
+
sendingRef.current = false;
|
|
1375
|
+
setSending(false);
|
|
1376
|
+
}
|
|
1377
|
+
};
|
|
1378
|
+
|
|
1379
|
+
const copyAll = async () => {
|
|
1380
|
+
const value = mergedText(segmentsRef.current);
|
|
1381
|
+
if (value === '') {
|
|
1382
|
+
showToast(t('insertEmpty'));
|
|
1383
|
+
return;
|
|
1384
|
+
}
|
|
1385
|
+
showToast((await copyText(value)) ? t('copied') : t('copyFailed'));
|
|
1386
|
+
};
|
|
1387
|
+
|
|
1388
|
+
// UI actions ----------------------------------------------------------
|
|
1389
|
+
const toggleCollapse = () => {
|
|
1390
|
+
setUi((previous) => clampUi({ ...previous, minimized: !previous.minimized }));
|
|
1391
|
+
};
|
|
1392
|
+
|
|
1393
|
+
const toggleHidden = () => {
|
|
1394
|
+
setUi((previous) => clampUi({ ...previous, hidden: !previous.hidden }));
|
|
1395
|
+
};
|
|
1396
|
+
|
|
1397
|
+
const toggleArchive = () => {
|
|
1398
|
+
const open = ui.showArchive !== true;
|
|
1399
|
+
setUi((previous) => ({ ...previous, showArchive: open }));
|
|
1400
|
+
if (open) {
|
|
1401
|
+
setTimeout(() => {
|
|
1402
|
+
const list = listRef.current;
|
|
1403
|
+
if (list !== null && list !== undefined) list.scrollTop = list.scrollHeight;
|
|
1404
|
+
}, 260);
|
|
1405
|
+
}
|
|
1406
|
+
};
|
|
1407
|
+
|
|
1408
|
+
const onSegmentChange = (event, id) => {
|
|
1409
|
+
const target = event.currentTarget ?? event.target;
|
|
1410
|
+
autoGrow(target);
|
|
1411
|
+
updateSegmentText(id, event.target?.value ?? '');
|
|
1412
|
+
};
|
|
1413
|
+
|
|
1414
|
+
const onSegmentKeyDown = (event, id) => {
|
|
1415
|
+
if (event.isComposing === true || event.nativeEvent?.isComposing === true) return;
|
|
1416
|
+
const chord = event.ctrlKey || event.metaKey;
|
|
1417
|
+
if (chord && event.key === 'Enter') {
|
|
1418
|
+
event.preventDefault();
|
|
1419
|
+
const current = segmentsRef.current.find((segment) => segment.id === id);
|
|
1420
|
+
if (current === undefined) return;
|
|
1421
|
+
const active = segmentsRef.current.filter((segment) => segment.archived !== true);
|
|
1422
|
+
const list = selectedRef.current.size > 0
|
|
1423
|
+
? active.filter((segment) => selectedRef.current.has(segment.id))
|
|
1424
|
+
: [current];
|
|
1425
|
+
insertSegments(list);
|
|
1426
|
+
return;
|
|
1427
|
+
}
|
|
1428
|
+
if (event.key !== 'Enter' || event.shiftKey || event.altKey || chord) return;
|
|
1429
|
+
event.preventDefault();
|
|
1430
|
+
splitSegment(id, event.currentTarget ?? event.target);
|
|
1431
|
+
};
|
|
1432
|
+
|
|
1433
|
+
const onHeaderPointerDown = (event) => {
|
|
1434
|
+
if (event.button !== undefined && event.button !== 0) return;
|
|
1435
|
+
const target = event.target;
|
|
1436
|
+
if (target !== null && typeof target?.closest === 'function' && target.closest('button') !== null) return;
|
|
1437
|
+
if (typeof ui.x !== 'number' || typeof ui.y !== 'number') return;
|
|
1438
|
+
const start = { px: event.clientX, py: event.clientY, x: ui.x, y: ui.y };
|
|
1439
|
+
dragRef.current = start;
|
|
1440
|
+
const move = (moveEvent) => {
|
|
1441
|
+
const vw = window.innerWidth || 1280;
|
|
1442
|
+
const vh = window.innerHeight || 800;
|
|
1443
|
+
const height = ui.minimized ? HEAD_H : ui.height;
|
|
1444
|
+
const x = Math.min(Math.max(8, start.x + moveEvent.clientX - start.px), Math.max(8, vw - ui.width - 8));
|
|
1445
|
+
const y = Math.min(Math.max(8, start.y + moveEvent.clientY - start.py), Math.max(8, vh - height - 8));
|
|
1446
|
+
setUi((previous) => ({ ...previous, x, y }));
|
|
1447
|
+
};
|
|
1448
|
+
const stop = () => {
|
|
1449
|
+
dragRef.current = null;
|
|
1450
|
+
window.removeEventListener('pointermove', move);
|
|
1451
|
+
window.removeEventListener('pointerup', stop);
|
|
1452
|
+
window.removeEventListener('pointercancel', stop);
|
|
1453
|
+
};
|
|
1454
|
+
window.addEventListener('pointermove', move);
|
|
1455
|
+
window.addEventListener('pointerup', stop);
|
|
1456
|
+
window.addEventListener('pointercancel', stop);
|
|
1457
|
+
event.preventDefault?.();
|
|
1458
|
+
};
|
|
1459
|
+
|
|
1460
|
+
const onResizePointerDown = (event) => {
|
|
1461
|
+
if (ui.minimized) return;
|
|
1462
|
+
if (event.button !== undefined && event.button !== 0) return;
|
|
1463
|
+
event.preventDefault?.();
|
|
1464
|
+
event.stopPropagation?.();
|
|
1465
|
+
const start = { px: event.clientX, py: event.clientY, width: ui.width, height: ui.height };
|
|
1466
|
+
resizeRef.current = start;
|
|
1467
|
+
setResizing(true);
|
|
1468
|
+
const move = (moveEvent) => {
|
|
1469
|
+
const vw = window.innerWidth || 1280;
|
|
1470
|
+
const vh = window.innerHeight || 800;
|
|
1471
|
+
const width = Math.min(Math.max(MIN_W, start.width + moveEvent.clientX - start.px), Math.max(MIN_W, vw - 16));
|
|
1472
|
+
const height = Math.min(Math.max(MIN_H, start.height + moveEvent.clientY - start.py), Math.max(MIN_H, vh - 16));
|
|
1473
|
+
setUi((previous) => {
|
|
1474
|
+
const maxX = Math.max(8, vw - width - 8);
|
|
1475
|
+
const maxY = Math.max(8, vh - height - 8);
|
|
1476
|
+
return {
|
|
1477
|
+
...previous,
|
|
1478
|
+
width,
|
|
1479
|
+
height,
|
|
1480
|
+
x: typeof previous.x === 'number' ? Math.min(previous.x, maxX) : previous.x,
|
|
1481
|
+
y: typeof previous.y === 'number' ? Math.min(previous.y, maxY) : previous.y,
|
|
1482
|
+
};
|
|
1483
|
+
});
|
|
1484
|
+
};
|
|
1485
|
+
const stop = () => {
|
|
1486
|
+
resizeRef.current = null;
|
|
1487
|
+
setResizing(false);
|
|
1488
|
+
window.removeEventListener('pointermove', move);
|
|
1489
|
+
window.removeEventListener('pointerup', stop);
|
|
1490
|
+
window.removeEventListener('pointercancel', stop);
|
|
1491
|
+
};
|
|
1492
|
+
window.addEventListener('pointermove', move);
|
|
1493
|
+
window.addEventListener('pointerup', stop);
|
|
1494
|
+
window.addEventListener('pointercancel', stop);
|
|
1495
|
+
};
|
|
1496
|
+
|
|
1497
|
+
// Render --------------------------------------------------------------
|
|
1498
|
+
if (binding === null || present !== true) return null;
|
|
1499
|
+
|
|
1500
|
+
const activeSegments = segments.filter((segment) => segment.archived !== true);
|
|
1501
|
+
const archivedSegments = segments.filter((segment) => segment.archived === true);
|
|
1502
|
+
const selectedActive = activeSegments.filter((segment) => selected.has(segment.id));
|
|
1503
|
+
const selectedCount = selectedActive.length;
|
|
1504
|
+
const allSelected = activeSegments.length > 0 && selectedCount === activeSegments.length;
|
|
1505
|
+
const totalChars = segments.reduce((sum, segment) => sum + segment.text.length, 0);
|
|
1506
|
+
const displayWidth = ui.width;
|
|
1507
|
+
const displayHeight = ui.minimized ? HEAD_H : ui.height;
|
|
1508
|
+
const fallbackX = Math.max(8, ((typeof window !== 'undefined' && window.innerWidth) || 1280) - displayWidth - 24);
|
|
1509
|
+
const fallbackY = Math.max(8, ((typeof window !== 'undefined' && window.innerHeight) || 800) - displayHeight - 24);
|
|
1510
|
+
const left = typeof ui.x === 'number' ? ui.x : fallbackX;
|
|
1511
|
+
const top = typeof ui.y === 'number' ? ui.y : fallbackY;
|
|
1512
|
+
|
|
1513
|
+
let statusText = '';
|
|
1514
|
+
if (status === 'loading') statusText = t('loading');
|
|
1515
|
+
else if (status === 'saving') statusText = t('saving');
|
|
1516
|
+
else if (status === 'dirty') statusText = t('unsaved');
|
|
1517
|
+
else if (status === 'error') statusText = loadFailedRef.current ? t('loadFailed') : t('saveFailed');
|
|
1518
|
+
else if (status === 'saved') {
|
|
1519
|
+
const clock = clockLabel(savedAt);
|
|
1520
|
+
statusText = clock === '' ? t('saved') : `${t('saved')} · ${clock}`;
|
|
1521
|
+
} else statusText = t('empty');
|
|
1522
|
+
|
|
1523
|
+
const archiveButtonTitle = ui.showArchive
|
|
1524
|
+
? t('hideArchive', { n: archivedSegments.length })
|
|
1525
|
+
: t('showArchive', { n: archivedSegments.length });
|
|
1526
|
+
|
|
1527
|
+
const header = h(
|
|
1528
|
+
'div',
|
|
1529
|
+
{ className: 'dsh-np-head', onPointerDown: onHeaderPointerDown },
|
|
1530
|
+
h('span', { className: 'dsh-np-dot', 'data-state': status }),
|
|
1531
|
+
h('span', { className: 'dsh-np-title' }, t('title')),
|
|
1532
|
+
h('span', { className: 'dsh-np-workspace', title: binding.cwd }, workspaceName(binding.cwd)),
|
|
1533
|
+
h('span', { className: 'dsh-np-spacer' }),
|
|
1534
|
+
h(
|
|
1535
|
+
'button',
|
|
1536
|
+
{
|
|
1537
|
+
type: 'button',
|
|
1538
|
+
className: 'dsh-np-btn',
|
|
1539
|
+
title: t('copy'),
|
|
1540
|
+
'aria-label': t('copy'),
|
|
1541
|
+
disabled: totalChars === 0,
|
|
1542
|
+
onClick: copyAll,
|
|
1543
|
+
},
|
|
1544
|
+
h(CopyIcon, { size: 15 }),
|
|
1545
|
+
),
|
|
1546
|
+
h(
|
|
1547
|
+
'button',
|
|
1548
|
+
{
|
|
1549
|
+
type: 'button',
|
|
1550
|
+
className: 'dsh-np-btn',
|
|
1551
|
+
'data-active': ui.showArchive ? 'true' : 'false',
|
|
1552
|
+
'aria-pressed': ui.showArchive ? 'true' : 'false',
|
|
1553
|
+
title: archiveButtonTitle,
|
|
1554
|
+
'aria-label': archiveButtonTitle,
|
|
1555
|
+
onClick: toggleArchive,
|
|
1556
|
+
},
|
|
1557
|
+
h(ArchiveIcon, { size: 15 }),
|
|
1558
|
+
archivedSegments.length > 0
|
|
1559
|
+
? h('span', { className: 'dsh-np-badge' }, String(archivedSegments.length))
|
|
1560
|
+
: null,
|
|
1561
|
+
),
|
|
1562
|
+
h(
|
|
1563
|
+
'button',
|
|
1564
|
+
{
|
|
1565
|
+
type: 'button',
|
|
1566
|
+
className: 'dsh-np-btn',
|
|
1567
|
+
title: t('addSegment'),
|
|
1568
|
+
'aria-label': t('addSegment'),
|
|
1569
|
+
onClick: addSegment,
|
|
1570
|
+
},
|
|
1571
|
+
h(PlusIcon, { size: 16 }),
|
|
1572
|
+
),
|
|
1573
|
+
h(
|
|
1574
|
+
'button',
|
|
1575
|
+
{
|
|
1576
|
+
type: 'button',
|
|
1577
|
+
className: 'dsh-np-btn',
|
|
1578
|
+
title: ui.minimized ? t('expand') : t('collapse'),
|
|
1579
|
+
'aria-label': ui.minimized ? t('expand') : t('collapse'),
|
|
1580
|
+
onClick: toggleCollapse,
|
|
1581
|
+
},
|
|
1582
|
+
h(ui.minimized ? ChevronsExpandIcon : ChevronsCollapseIcon, { size: 15 }),
|
|
1583
|
+
),
|
|
1584
|
+
h(
|
|
1585
|
+
'button',
|
|
1586
|
+
{
|
|
1587
|
+
type: 'button',
|
|
1588
|
+
className: 'dsh-np-btn',
|
|
1589
|
+
title: t('minimize'),
|
|
1590
|
+
'aria-label': t('minimize'),
|
|
1591
|
+
onClick: toggleHidden,
|
|
1592
|
+
},
|
|
1593
|
+
h(MinimizeIcon, { size: 15 }),
|
|
1594
|
+
),
|
|
1595
|
+
);
|
|
1596
|
+
|
|
1597
|
+
const tools = h(
|
|
1598
|
+
'div',
|
|
1599
|
+
{ className: 'dsh-np-tools' },
|
|
1600
|
+
h(
|
|
1601
|
+
'label',
|
|
1602
|
+
{ className: 'dsh-np-selectall', title: t('selectAll') },
|
|
1603
|
+
h('input', {
|
|
1604
|
+
type: 'checkbox',
|
|
1605
|
+
checked: allSelected,
|
|
1606
|
+
disabled: activeSegments.length === 0,
|
|
1607
|
+
'aria-label': t('selectAll'),
|
|
1608
|
+
onChange: toggleSelectAll,
|
|
1609
|
+
}),
|
|
1610
|
+
h('span', { className: 'dsh-np-selcount' },
|
|
1611
|
+
selectedCount > 0 ? t('selected', { n: selectedCount }) : t('selectHint')),
|
|
1612
|
+
),
|
|
1613
|
+
h('span', { className: 'dsh-np-spacer' }),
|
|
1614
|
+
h(
|
|
1615
|
+
'button',
|
|
1616
|
+
{
|
|
1617
|
+
type: 'button',
|
|
1618
|
+
className: 'dsh-np-btn',
|
|
1619
|
+
title: selectedCount > 0 ? t('insertSelected', { n: selectedCount }) : t('insert'),
|
|
1620
|
+
'aria-label': t('insert'),
|
|
1621
|
+
disabled: selectedCount === 0,
|
|
1622
|
+
onClick: () => insertSegments(selectedActive),
|
|
1623
|
+
},
|
|
1624
|
+
h(InsertIcon, { size: 14 }),
|
|
1625
|
+
),
|
|
1626
|
+
h(
|
|
1627
|
+
'button',
|
|
1628
|
+
{
|
|
1629
|
+
type: 'button',
|
|
1630
|
+
className: 'dsh-np-btn',
|
|
1631
|
+
title: selectedCount > 0 ? t('replaceSelected', { n: selectedCount }) : t('replace'),
|
|
1632
|
+
'aria-label': t('replace'),
|
|
1633
|
+
disabled: selectedCount === 0,
|
|
1634
|
+
onClick: () => replaceSegments(selectedActive),
|
|
1635
|
+
},
|
|
1636
|
+
h(ReplaceIcon, { size: 14 }),
|
|
1637
|
+
),
|
|
1638
|
+
h(
|
|
1639
|
+
'button',
|
|
1640
|
+
{
|
|
1641
|
+
type: 'button',
|
|
1642
|
+
className: 'dsh-np-btn',
|
|
1643
|
+
'data-accent': 'true',
|
|
1644
|
+
title: selectedCount > 0
|
|
1645
|
+
? (sending ? t('sending') : t('sendSelected', { n: selectedCount }))
|
|
1646
|
+
: t('send'),
|
|
1647
|
+
'aria-label': t('send'),
|
|
1648
|
+
disabled: selectedCount === 0 || sending,
|
|
1649
|
+
onClick: () => sendSegments(selectedActive),
|
|
1650
|
+
},
|
|
1651
|
+
h(SendIcon, { size: 14 }),
|
|
1652
|
+
),
|
|
1653
|
+
h(
|
|
1654
|
+
'button',
|
|
1655
|
+
{
|
|
1656
|
+
type: 'button',
|
|
1657
|
+
className: 'dsh-np-btn',
|
|
1658
|
+
title: selectedCount > 0 ? t('archiveSelected', { n: selectedCount }) : t('archive'),
|
|
1659
|
+
'aria-label': t('archive'),
|
|
1660
|
+
disabled: selectedCount === 0,
|
|
1661
|
+
onClick: archiveSelected,
|
|
1662
|
+
},
|
|
1663
|
+
h(ArchiveIcon, { size: 14 }),
|
|
1664
|
+
),
|
|
1665
|
+
selectedCount > 0
|
|
1666
|
+
? h(
|
|
1667
|
+
'button',
|
|
1668
|
+
{
|
|
1669
|
+
type: 'button',
|
|
1670
|
+
className: 'dsh-np-btn',
|
|
1671
|
+
title: t('clearSelection'),
|
|
1672
|
+
'aria-label': t('clearSelection'),
|
|
1673
|
+
onClick: () => setSelection(new Set()),
|
|
1674
|
+
},
|
|
1675
|
+
h(CloseIcon, { size: 13 }),
|
|
1676
|
+
)
|
|
1677
|
+
: null,
|
|
1678
|
+
);
|
|
1679
|
+
|
|
1680
|
+
const activeCards = activeSegments.map((segment, index) => h(
|
|
1681
|
+
'article',
|
|
1682
|
+
{
|
|
1683
|
+
key: segment.id,
|
|
1684
|
+
className: 'dsh-np-seg',
|
|
1685
|
+
'data-seg-id': segment.id,
|
|
1686
|
+
'data-archived': 'false',
|
|
1687
|
+
'data-selected': selected.has(segment.id) ? 'true' : 'false',
|
|
1688
|
+
},
|
|
1689
|
+
h(
|
|
1690
|
+
'div',
|
|
1691
|
+
{ className: 'dsh-np-seg-head' },
|
|
1692
|
+
h(
|
|
1693
|
+
'label',
|
|
1694
|
+
{ className: 'dsh-np-check', title: t('selectSegment') },
|
|
1695
|
+
h('input', {
|
|
1696
|
+
type: 'checkbox',
|
|
1697
|
+
checked: selected.has(segment.id),
|
|
1698
|
+
'aria-label': t('selectSegment'),
|
|
1699
|
+
onChange: () => toggleSelected(segment.id),
|
|
1700
|
+
}),
|
|
1701
|
+
),
|
|
1702
|
+
h('span', { className: 'dsh-np-index' }, String(index + 1).padStart(2, '0')),
|
|
1703
|
+
h('span', { className: 'dsh-np-spacer' }),
|
|
1704
|
+
h(
|
|
1705
|
+
'button',
|
|
1706
|
+
{
|
|
1707
|
+
type: 'button',
|
|
1708
|
+
className: 'dsh-np-segbtn',
|
|
1709
|
+
title: t('insertSegment'),
|
|
1710
|
+
'aria-label': t('insertSegment'),
|
|
1711
|
+
disabled: segment.text.trim() === '',
|
|
1712
|
+
onClick: () => insertSegments([segment]),
|
|
1713
|
+
},
|
|
1714
|
+
h(InsertIcon, { size: 14 }),
|
|
1715
|
+
),
|
|
1716
|
+
h(
|
|
1717
|
+
'button',
|
|
1718
|
+
{
|
|
1719
|
+
type: 'button',
|
|
1720
|
+
className: 'dsh-np-segbtn',
|
|
1721
|
+
title: t('replaceSegment'),
|
|
1722
|
+
'aria-label': t('replaceSegment'),
|
|
1723
|
+
disabled: segment.text.trim() === '',
|
|
1724
|
+
onClick: () => replaceSegments([segment]),
|
|
1725
|
+
},
|
|
1726
|
+
h(ReplaceIcon, { size: 14 }),
|
|
1727
|
+
),
|
|
1728
|
+
h(
|
|
1729
|
+
'button',
|
|
1730
|
+
{
|
|
1731
|
+
type: 'button',
|
|
1732
|
+
className: 'dsh-np-segbtn',
|
|
1733
|
+
title: t('sendSegment'),
|
|
1734
|
+
'aria-label': t('sendSegment'),
|
|
1735
|
+
disabled: segment.text.trim() === '' || sending,
|
|
1736
|
+
onClick: () => sendSegments([segment]),
|
|
1737
|
+
},
|
|
1738
|
+
h(SendIcon, { size: 14 }),
|
|
1739
|
+
),
|
|
1740
|
+
h(
|
|
1741
|
+
'button',
|
|
1742
|
+
{
|
|
1743
|
+
type: 'button',
|
|
1744
|
+
className: 'dsh-np-segbtn',
|
|
1745
|
+
title: t('archiveSegment'),
|
|
1746
|
+
'aria-label': t('archiveSegment'),
|
|
1747
|
+
onClick: () => archiveSegment(segment.id),
|
|
1748
|
+
},
|
|
1749
|
+
h(ArchiveIcon, { size: 14 }),
|
|
1750
|
+
),
|
|
1751
|
+
),
|
|
1752
|
+
h('textarea', {
|
|
1753
|
+
ref: (element) => {
|
|
1754
|
+
if (element === null || element === undefined) {
|
|
1755
|
+
textareaRefs.current.delete(segment.id);
|
|
1756
|
+
return;
|
|
1757
|
+
}
|
|
1758
|
+
textareaRefs.current.set(segment.id, element);
|
|
1759
|
+
autoGrow(element);
|
|
1760
|
+
},
|
|
1761
|
+
className: 'dsh-np-seg-text',
|
|
1762
|
+
rows: 1,
|
|
1763
|
+
value: segment.text,
|
|
1764
|
+
placeholder: index === 0 ? t('placeholder') : t('segmentPlaceholder'),
|
|
1765
|
+
spellCheck: false,
|
|
1766
|
+
maxLength: MAX_SEGMENT_CHARS,
|
|
1767
|
+
'aria-label': t('editor'),
|
|
1768
|
+
onChange: (event) => onSegmentChange(event, segment.id),
|
|
1769
|
+
onKeyDown: (event) => onSegmentKeyDown(event, segment.id),
|
|
1770
|
+
onBlur: () => {
|
|
1771
|
+
if (dirtyRef.current) saveNowRef.current?.();
|
|
1772
|
+
},
|
|
1773
|
+
}),
|
|
1774
|
+
));
|
|
1775
|
+
|
|
1776
|
+
const activeEmpty = activeSegments.length === 0
|
|
1777
|
+
? h(
|
|
1778
|
+
'div',
|
|
1779
|
+
{ key: 'active-empty', className: 'dsh-np-empty' },
|
|
1780
|
+
h('span', null, t('noActive')),
|
|
1781
|
+
h(
|
|
1782
|
+
'button',
|
|
1783
|
+
{ type: 'button', className: 'dsh-np-empty-add', onClick: addSegment },
|
|
1784
|
+
h(PlusIcon, { size: 14 }),
|
|
1785
|
+
h('span', null, t('addSegment')),
|
|
1786
|
+
),
|
|
1787
|
+
)
|
|
1788
|
+
: null;
|
|
1789
|
+
|
|
1790
|
+
const archiveSection = archivedSegments.length > 0 || ui.showArchive
|
|
1791
|
+
? h(
|
|
1792
|
+
'div',
|
|
1793
|
+
{
|
|
1794
|
+
key: 'archive',
|
|
1795
|
+
className: 'dsh-np-archive',
|
|
1796
|
+
'data-open': ui.showArchive ? 'true' : 'false',
|
|
1797
|
+
'aria-hidden': ui.showArchive ? undefined : 'true',
|
|
1798
|
+
},
|
|
1799
|
+
h(
|
|
1800
|
+
'div',
|
|
1801
|
+
{ className: 'dsh-np-archive-inner' },
|
|
1802
|
+
h(
|
|
1803
|
+
'div',
|
|
1804
|
+
{ className: 'dsh-np-archive-head' },
|
|
1805
|
+
h(ArchiveIcon, { size: 13 }),
|
|
1806
|
+
h('span', { className: 'dsh-np-archive-label' },
|
|
1807
|
+
t('archiveCount', { n: archivedSegments.length })),
|
|
1808
|
+
h('span', { className: 'dsh-np-spacer' }),
|
|
1809
|
+
archivedSegments.length > 0
|
|
1810
|
+
? h('button', {
|
|
1811
|
+
type: 'button',
|
|
1812
|
+
className: 'dsh-np-link',
|
|
1813
|
+
title: t('restoreAll'),
|
|
1814
|
+
onClick: restoreAll,
|
|
1815
|
+
}, t('restoreAll'))
|
|
1816
|
+
: null,
|
|
1817
|
+
archivedSegments.length > 0
|
|
1818
|
+
? h('button', {
|
|
1819
|
+
type: 'button',
|
|
1820
|
+
className: 'dsh-np-link',
|
|
1821
|
+
'data-danger': 'true',
|
|
1822
|
+
title: t('clearArchive'),
|
|
1823
|
+
onClick: clearArchive,
|
|
1824
|
+
}, t('clearArchive'))
|
|
1825
|
+
: null,
|
|
1826
|
+
),
|
|
1827
|
+
archivedSegments.length === 0
|
|
1828
|
+
? h('div', { className: 'dsh-np-empty-small' }, t('archiveEmpty'))
|
|
1829
|
+
: h(
|
|
1830
|
+
'div',
|
|
1831
|
+
{ className: 'dsh-np-archive-list' },
|
|
1832
|
+
archivedSegments.map((segment) => h(
|
|
1833
|
+
'article',
|
|
1834
|
+
{
|
|
1835
|
+
key: segment.id,
|
|
1836
|
+
className: 'dsh-np-seg',
|
|
1837
|
+
'data-seg-id': segment.id,
|
|
1838
|
+
'data-archived': 'true',
|
|
1839
|
+
},
|
|
1840
|
+
h(
|
|
1841
|
+
'div',
|
|
1842
|
+
{ className: 'dsh-np-seg-head' },
|
|
1843
|
+
h('span', { className: 'dsh-np-archived-chip' }, t('archived')),
|
|
1844
|
+
h('span', { className: 'dsh-np-spacer' }),
|
|
1845
|
+
h(
|
|
1846
|
+
'button',
|
|
1847
|
+
{
|
|
1848
|
+
type: 'button',
|
|
1849
|
+
className: 'dsh-np-segbtn',
|
|
1850
|
+
title: t('insertSegment'),
|
|
1851
|
+
'aria-label': t('insertSegment'),
|
|
1852
|
+
disabled: segment.text.trim() === '',
|
|
1853
|
+
onClick: () => insertSegments([segment]),
|
|
1854
|
+
},
|
|
1855
|
+
h(InsertIcon, { size: 14 }),
|
|
1856
|
+
),
|
|
1857
|
+
h(
|
|
1858
|
+
'button',
|
|
1859
|
+
{
|
|
1860
|
+
type: 'button',
|
|
1861
|
+
className: 'dsh-np-segbtn',
|
|
1862
|
+
title: t('replaceSegment'),
|
|
1863
|
+
'aria-label': t('replaceSegment'),
|
|
1864
|
+
disabled: segment.text.trim() === '',
|
|
1865
|
+
onClick: () => replaceSegments([segment]),
|
|
1866
|
+
},
|
|
1867
|
+
h(ReplaceIcon, { size: 14 }),
|
|
1868
|
+
),
|
|
1869
|
+
h(
|
|
1870
|
+
'button',
|
|
1871
|
+
{
|
|
1872
|
+
type: 'button',
|
|
1873
|
+
className: 'dsh-np-segbtn',
|
|
1874
|
+
title: t('restoreSegment'),
|
|
1875
|
+
'aria-label': t('restoreSegment'),
|
|
1876
|
+
onClick: () => restoreSegment(segment.id),
|
|
1877
|
+
},
|
|
1878
|
+
h(RestoreIcon, { size: 14 }),
|
|
1879
|
+
),
|
|
1880
|
+
h(
|
|
1881
|
+
'button',
|
|
1882
|
+
{
|
|
1883
|
+
type: 'button',
|
|
1884
|
+
className: 'dsh-np-segbtn',
|
|
1885
|
+
'data-danger': 'true',
|
|
1886
|
+
title: t('deleteSegment'),
|
|
1887
|
+
'aria-label': t('deleteSegment'),
|
|
1888
|
+
onClick: () => deleteArchived(segment.id),
|
|
1889
|
+
},
|
|
1890
|
+
h(TrashIcon, { size: 14 }),
|
|
1891
|
+
),
|
|
1892
|
+
),
|
|
1893
|
+
h('div', { className: 'dsh-np-seg-archived-text' },
|
|
1894
|
+
segment.text === '' ? t('emptySegment') : segment.text),
|
|
1895
|
+
)),
|
|
1896
|
+
),
|
|
1897
|
+
),
|
|
1898
|
+
)
|
|
1899
|
+
: null;
|
|
1900
|
+
|
|
1901
|
+
const listContent = status === 'loading'
|
|
1902
|
+
? h('div', { key: 'loading', className: 'dsh-np-empty' }, t('loading'))
|
|
1903
|
+
: [activeCards, activeEmpty, archiveSection];
|
|
1904
|
+
|
|
1905
|
+
const footer = h(
|
|
1906
|
+
'div',
|
|
1907
|
+
{ className: 'dsh-np-foot' },
|
|
1908
|
+
h('span', {
|
|
1909
|
+
className: 'dsh-np-status',
|
|
1910
|
+
'data-state': status,
|
|
1911
|
+
title: error === '' ? undefined : error,
|
|
1912
|
+
}, statusText),
|
|
1913
|
+
status === 'error'
|
|
1914
|
+
? h('button', {
|
|
1915
|
+
type: 'button',
|
|
1916
|
+
className: 'dsh-np-retry',
|
|
1917
|
+
onClick: () => {
|
|
1918
|
+
if (loadFailedRef.current) reloadRef.current?.();
|
|
1919
|
+
else saveNowRef.current?.({ force: true });
|
|
1920
|
+
},
|
|
1921
|
+
}, t('retry'))
|
|
1922
|
+
: null,
|
|
1923
|
+
h('span', { className: 'dsh-np-count' },
|
|
1924
|
+
`${t('chars', { n: totalChars })} · ${t('segments', { n: activeSegments.length })}`),
|
|
1925
|
+
h('span', { className: 'dsh-np-hint', title: t('hint') }, t('hint')),
|
|
1926
|
+
);
|
|
1927
|
+
|
|
1928
|
+
const toastNode = toast === null
|
|
1929
|
+
? null
|
|
1930
|
+
: h(
|
|
1931
|
+
'div',
|
|
1932
|
+
{ className: 'dsh-np-toast', role: 'status' },
|
|
1933
|
+
h('span', { className: 'dsh-np-toast-text' }, toast.message),
|
|
1934
|
+
toast.action !== null && toast.action !== undefined
|
|
1935
|
+
? h('button', {
|
|
1936
|
+
type: 'button',
|
|
1937
|
+
className: 'dsh-np-toast-action',
|
|
1938
|
+
onClick: toast.action.onClick,
|
|
1939
|
+
}, toast.action.label)
|
|
1940
|
+
: null,
|
|
1941
|
+
);
|
|
1942
|
+
|
|
1943
|
+
const body = h(
|
|
1944
|
+
'div',
|
|
1945
|
+
{
|
|
1946
|
+
className: 'dsh-np-body',
|
|
1947
|
+
'aria-hidden': ui.minimized ? 'true' : undefined,
|
|
1948
|
+
},
|
|
1949
|
+
tools,
|
|
1950
|
+
h('div', { className: 'dsh-np-list', ref: listRef }, listContent),
|
|
1951
|
+
footer,
|
|
1952
|
+
toastNode,
|
|
1953
|
+
);
|
|
1954
|
+
|
|
1955
|
+
const resizeHandle = ui.minimized
|
|
1956
|
+
? null
|
|
1957
|
+
: h('div', {
|
|
1958
|
+
className: 'dsh-np-resize',
|
|
1959
|
+
role: 'separator',
|
|
1960
|
+
title: t('resize'),
|
|
1961
|
+
'aria-label': t('resize'),
|
|
1962
|
+
onPointerDown: onResizePointerDown,
|
|
1963
|
+
});
|
|
1964
|
+
|
|
1965
|
+
const card = h(
|
|
1966
|
+
'section',
|
|
1967
|
+
{
|
|
1968
|
+
key: 'card',
|
|
1969
|
+
className: 'dsh-np-card',
|
|
1970
|
+
'data-dsh-notepad': 'true',
|
|
1971
|
+
'data-minimized': ui.minimized ? 'true' : 'false',
|
|
1972
|
+
'data-hidden': ui.hidden ? 'true' : 'false',
|
|
1973
|
+
'data-resizing': resizing ? 'true' : 'false',
|
|
1974
|
+
'data-state': status,
|
|
1975
|
+
'aria-hidden': ui.hidden ? 'true' : undefined,
|
|
1976
|
+
style: {
|
|
1977
|
+
left: `${left}px`,
|
|
1978
|
+
top: `${top}px`,
|
|
1979
|
+
width: `${displayWidth}px`,
|
|
1980
|
+
height: `${displayHeight}px`,
|
|
1981
|
+
},
|
|
1982
|
+
role: 'complementary',
|
|
1983
|
+
'aria-label': t('title'),
|
|
1984
|
+
},
|
|
1985
|
+
header,
|
|
1986
|
+
body,
|
|
1987
|
+
resizeHandle,
|
|
1988
|
+
);
|
|
1989
|
+
|
|
1990
|
+
const restore = h(
|
|
1991
|
+
'button',
|
|
1992
|
+
{
|
|
1993
|
+
key: 'restore',
|
|
1994
|
+
type: 'button',
|
|
1995
|
+
className: 'dsh-np-restore',
|
|
1996
|
+
'data-visible': ui.hidden ? 'true' : 'false',
|
|
1997
|
+
'aria-hidden': ui.hidden ? undefined : 'true',
|
|
1998
|
+
tabIndex: ui.hidden ? undefined : -1,
|
|
1999
|
+
title: t('show'),
|
|
2000
|
+
'aria-label': t('show'),
|
|
2001
|
+
onClick: toggleHidden,
|
|
2002
|
+
},
|
|
2003
|
+
h(NotepadIcon, { size: 16 }),
|
|
2004
|
+
);
|
|
2005
|
+
|
|
2006
|
+
return [card, restore];
|
|
2007
|
+
}
|
|
2008
|
+
|
|
2009
|
+
// ------------------------------------------------------------------
|
|
2010
|
+
// Plugin body
|
|
2011
|
+
// ------------------------------------------------------------------
|
|
2012
|
+
function apply(ctx) {
|
|
2013
|
+
let translate = fallbackTranslate();
|
|
2014
|
+
|
|
2015
|
+
try {
|
|
2016
|
+
const style = document.createElement('style');
|
|
2017
|
+
style.setAttribute('data-dsh-notepad', '');
|
|
2018
|
+
style.textContent = CSS;
|
|
2019
|
+
document.head.appendChild(style);
|
|
2020
|
+
ctx.effect(() => () => style.remove(), 'dsh-notepad: styles');
|
|
2021
|
+
} catch (error) {
|
|
2022
|
+
console.warn('[dsh-notepad] style injection failed:', error?.message ?? error);
|
|
2023
|
+
}
|
|
2024
|
+
|
|
2025
|
+
ctx.effect(() => {
|
|
2026
|
+
const locale = ctx.get('locale');
|
|
2027
|
+
if (locale === undefined) return undefined;
|
|
2028
|
+
let dispose;
|
|
2029
|
+
try {
|
|
2030
|
+
dispose = locale.register(NS, { zh: DICT.zh, en: DICT.en });
|
|
2031
|
+
translate = locale.bind(NS);
|
|
2032
|
+
} catch (error) {
|
|
2033
|
+
console.warn('[dsh-notepad] locale registration failed:', error?.message ?? error);
|
|
2034
|
+
return undefined;
|
|
2035
|
+
}
|
|
2036
|
+
return () => {
|
|
2037
|
+
translate = fallbackTranslate();
|
|
2038
|
+
try {
|
|
2039
|
+
dispose?.();
|
|
2040
|
+
} catch {
|
|
2041
|
+
/* Removing a dictionary must never break unload. */
|
|
2042
|
+
}
|
|
2043
|
+
};
|
|
2044
|
+
}, 'dsh-notepad: dictionaries');
|
|
2045
|
+
|
|
2046
|
+
const slots = ctx.get('slots');
|
|
2047
|
+
if (slots === undefined || typeof slots.register !== 'function' || typeof slots.inject !== 'function') {
|
|
2048
|
+
console.warn('[dsh-notepad] slots service missing; floating notepad disabled');
|
|
2049
|
+
return;
|
|
2050
|
+
}
|
|
2051
|
+
|
|
2052
|
+
try {
|
|
2053
|
+
ctx.effect(
|
|
2054
|
+
() => slots.inject('shell.overlay', () => slots.register(
|
|
2055
|
+
{ name: 'shell.overlay', id: 'notepad', order: 70 },
|
|
2056
|
+
() => h(NotepadOverlay, { ctx, t: (key, params) => translate(key, params) }),
|
|
2057
|
+
)),
|
|
2058
|
+
'dsh-notepad: overlay',
|
|
2059
|
+
);
|
|
2060
|
+
} catch (error) {
|
|
2061
|
+
console.warn('[dsh-notepad] could not register shell.overlay:', error?.message ?? error);
|
|
2062
|
+
}
|
|
2063
|
+
}
|
|
2064
|
+
|
|
2065
|
+
return {
|
|
2066
|
+
inject: ['slots'],
|
|
2067
|
+
apply,
|
|
2068
|
+
};
|
|
2069
|
+
},
|
|
2070
|
+
});
|
|
2071
|
+
}
|
|
2072
|
+
})();
|