@restregular/dsh-notepad 0.1.0 → 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/README.md +66 -21
- package/client.js +1342 -217
- package/index.js +4 -2
- package/lib/note-store.js +158 -25
- package/locale/en.json +1 -1
- package/locale/zh.json +1 -1
- package/package.json +2 -2
package/client.js
CHANGED
|
@@ -5,9 +5,18 @@
|
|
|
5
5
|
* the workspace of the session currently retained by the main view, so every
|
|
6
6
|
* session opened from the same workspace shows the same note.
|
|
7
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
|
+
*
|
|
8
17
|
* Data lives on the Host side at `<workspace>/.dsh/notepad.json` through the
|
|
9
|
-
* same-origin route `/dsh-notepad/api/note`;
|
|
10
|
-
*
|
|
18
|
+
* same-origin route `/dsh-notepad/api/note`; the widget's position, size,
|
|
19
|
+
* collapsed/hidden state and archive visibility are kept in localStorage.
|
|
11
20
|
*
|
|
12
21
|
* The whole file is wrapped in an IIFE: client bundles from every plugin are
|
|
13
22
|
* concatenated into the same classic-script realm, so top-level bindings would
|
|
@@ -33,10 +42,14 @@ window.__ModuleLoader__.load({
|
|
|
33
42
|
const NS = 'dsh-notepad';
|
|
34
43
|
const API = '/dsh-notepad/api';
|
|
35
44
|
const UI_KEY = 'dsh-notepad.ui.v1';
|
|
36
|
-
const
|
|
37
|
-
const
|
|
38
|
-
const
|
|
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;
|
|
39
50
|
const SAVE_DELAY = 500;
|
|
51
|
+
const MAX_SEGMENTS = 200;
|
|
52
|
+
const MAX_SEGMENT_CHARS = 160000;
|
|
40
53
|
|
|
41
54
|
// ------------------------------------------------------------------
|
|
42
55
|
// Copy
|
|
@@ -44,55 +57,149 @@ window.__ModuleLoader__.load({
|
|
|
44
57
|
const DICT = {
|
|
45
58
|
zh: {
|
|
46
59
|
title: '记事本',
|
|
47
|
-
editor: '
|
|
60
|
+
editor: '分段内容',
|
|
48
61
|
placeholder: '记录开发中遇到的问题,或下一阶段要做的事……',
|
|
62
|
+
segmentPlaceholder: '新分段…',
|
|
49
63
|
loading: '读取中…',
|
|
50
64
|
empty: '还没有内容',
|
|
51
65
|
unsaved: '未保存',
|
|
52
66
|
saving: '保存中…',
|
|
53
67
|
saved: '已保存',
|
|
54
68
|
saveFailed: '保存失败',
|
|
69
|
+
loadFailed: '读取失败',
|
|
55
70
|
retry: '重试',
|
|
56
71
|
insert: '插入到输入框',
|
|
72
|
+
insertSegment: '插入此段',
|
|
73
|
+
insertSelected: '插入选中的 {n} 段',
|
|
57
74
|
inserted: '已插入输入框',
|
|
58
|
-
insertEmpty: '
|
|
75
|
+
insertEmpty: '没有可插入的内容',
|
|
59
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: '发送接口不可用,已插入输入框',
|
|
60
89
|
copy: '复制全部',
|
|
61
90
|
copied: '已复制',
|
|
62
91
|
copyFailed: '复制失败',
|
|
63
|
-
|
|
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: '折叠',
|
|
64
122
|
expand: '展开',
|
|
65
|
-
|
|
123
|
+
minimize: '最小化',
|
|
66
124
|
show: '显示记事本(Ctrl+Shift+N)',
|
|
67
125
|
chars: '{n} 字符',
|
|
68
|
-
|
|
126
|
+
segments: '{n} 段',
|
|
127
|
+
hint: 'Enter 新建分段 · Shift+Enter 换行 · Ctrl+Enter 插入',
|
|
69
128
|
workspace: '工作区',
|
|
129
|
+
resize: '拖动调整大小',
|
|
70
130
|
},
|
|
71
131
|
en: {
|
|
72
132
|
title: 'Notepad',
|
|
73
|
-
editor: '
|
|
133
|
+
editor: 'Segment content',
|
|
74
134
|
placeholder: 'Jot down problems, or what to build next…',
|
|
135
|
+
segmentPlaceholder: 'New segment…',
|
|
75
136
|
loading: 'Loading…',
|
|
76
137
|
empty: 'Nothing here yet',
|
|
77
138
|
unsaved: 'Unsaved',
|
|
78
139
|
saving: 'Saving…',
|
|
79
140
|
saved: 'Saved',
|
|
80
141
|
saveFailed: 'Save failed',
|
|
142
|
+
loadFailed: 'Load failed',
|
|
81
143
|
retry: 'Retry',
|
|
82
144
|
insert: 'Insert into composer',
|
|
83
|
-
|
|
84
|
-
|
|
145
|
+
insertSegment: 'Insert this segment',
|
|
146
|
+
insertSelected: 'Insert {n} selected',
|
|
147
|
+
inserted: 'Inserted into composer',
|
|
148
|
+
insertEmpty: 'Nothing to insert',
|
|
85
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',
|
|
86
162
|
copy: 'Copy all',
|
|
87
163
|
copied: 'Copied',
|
|
88
164
|
copyFailed: 'Copy failed',
|
|
89
|
-
|
|
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',
|
|
90
195
|
expand: 'Expand',
|
|
91
|
-
|
|
196
|
+
minimize: 'Minimize',
|
|
92
197
|
show: 'Show notepad (Ctrl+Shift+N)',
|
|
93
198
|
chars: '{n} chars',
|
|
94
|
-
|
|
199
|
+
segments: '{n} segments',
|
|
200
|
+
hint: 'Enter new segment · Shift+Enter newline · Ctrl+Enter insert',
|
|
95
201
|
workspace: 'Workspace',
|
|
202
|
+
resize: 'Drag to resize',
|
|
96
203
|
},
|
|
97
204
|
};
|
|
98
205
|
|
|
@@ -150,18 +257,55 @@ window.__ModuleLoader__.load({
|
|
|
150
257
|
);
|
|
151
258
|
}
|
|
152
259
|
|
|
260
|
+
/** Arrow pointing to the bottom-left corner. */
|
|
153
261
|
function InsertIcon({ size = 16 }) {
|
|
154
262
|
return h(
|
|
155
263
|
Svg,
|
|
156
264
|
{ size, viewBox: '0 0 16 16' },
|
|
157
265
|
h('path', {
|
|
158
|
-
d: '
|
|
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',
|
|
159
284
|
stroke: 'currentColor', strokeWidth: 1.35,
|
|
160
285
|
strokeLinecap: 'round', strokeLinejoin: 'round',
|
|
161
286
|
}),
|
|
162
287
|
h('path', {
|
|
163
|
-
d: '
|
|
164
|
-
stroke: 'currentColor', strokeWidth: 1.35,
|
|
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',
|
|
165
309
|
}),
|
|
166
310
|
);
|
|
167
311
|
}
|
|
@@ -181,94 +325,314 @@ window.__ModuleLoader__.load({
|
|
|
181
325
|
);
|
|
182
326
|
}
|
|
183
327
|
|
|
184
|
-
function
|
|
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 }) {
|
|
185
384
|
return h(
|
|
186
385
|
Svg,
|
|
187
386
|
{ size, viewBox: '0 0 16 16' },
|
|
188
387
|
h('path', {
|
|
189
|
-
d: '
|
|
388
|
+
d: 'M8 3.2v9.6M3.2 8h9.6',
|
|
190
389
|
stroke: 'currentColor', strokeWidth: 1.4, strokeLinecap: 'round',
|
|
191
390
|
}),
|
|
192
391
|
);
|
|
193
392
|
}
|
|
194
393
|
|
|
195
|
-
function
|
|
394
|
+
function CloseIcon({ size = 16 }) {
|
|
196
395
|
return h(
|
|
197
396
|
Svg,
|
|
198
397
|
{ size, viewBox: '0 0 16 16' },
|
|
199
398
|
h('path', {
|
|
200
|
-
d: '
|
|
201
|
-
stroke: 'currentColor', strokeWidth: 1.4,
|
|
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,
|
|
202
418
|
strokeLinecap: 'round', strokeLinejoin: 'round',
|
|
203
419
|
}),
|
|
204
420
|
);
|
|
205
421
|
}
|
|
206
422
|
|
|
207
|
-
|
|
423
|
+
/** Chevrons opening away from each other (expand). */
|
|
424
|
+
function ChevronsExpandIcon({ size = 16 }) {
|
|
208
425
|
return h(
|
|
209
426
|
Svg,
|
|
210
427
|
{ size, viewBox: '0 0 16 16' },
|
|
211
428
|
h('path', {
|
|
212
|
-
d: '
|
|
213
|
-
stroke: 'currentColor', strokeWidth: 1.
|
|
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,
|
|
214
436
|
strokeLinecap: 'round', strokeLinejoin: 'round',
|
|
215
437
|
}),
|
|
216
|
-
|
|
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' },
|
|
217
446
|
h('path', {
|
|
218
|
-
d: '
|
|
447
|
+
d: 'M4.2 4.2 11.4 11.4',
|
|
219
448
|
stroke: 'currentColor', strokeWidth: 1.35, strokeLinecap: 'round',
|
|
220
449
|
}),
|
|
450
|
+
h('path', {
|
|
451
|
+
d: 'M6.8 11.6h4.8V6.8',
|
|
452
|
+
stroke: 'currentColor', strokeWidth: 1.35,
|
|
453
|
+
strokeLinecap: 'round', strokeLinejoin: 'round',
|
|
454
|
+
}),
|
|
221
455
|
);
|
|
222
456
|
}
|
|
223
457
|
|
|
224
458
|
// ------------------------------------------------------------------
|
|
225
459
|
// Styles
|
|
226
460
|
// ------------------------------------------------------------------
|
|
227
|
-
const
|
|
228
|
-
|
|
229
|
-
.dsh-np-
|
|
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)))}
|
|
230
467
|
.dsh-np-card[data-minimized=true] .dsh-np-head{border-bottom-color:transparent}
|
|
231
468
|
.dsh-np-head:active{cursor:grabbing}
|
|
232
469
|
.dsh-np-dot{flex:none;width:7px;height:7px;border-radius:50%;background:var(--dsw-alias-label-caption,#9aa3b2);transition:background .15s}
|
|
233
|
-
.dsh-np-dot[data-state=dirty]{background
|
|
234
|
-
.dsh-np-dot[data-state=saving]{
|
|
235
|
-
.dsh-np-dot[data-state=error]{background
|
|
236
|
-
.dsh-np-dot[data-state=saved]{background
|
|
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)}
|
|
237
474
|
.dsh-np-title{flex:none;font-size:12.5px;font-weight:650;letter-spacing:.01em;white-space:nowrap}
|
|
238
|
-
.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}
|
|
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}
|
|
239
476
|
.dsh-np-spacer{flex:1 1 auto;min-width:0}
|
|
240
|
-
.dsh-np-btn{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}
|
|
241
|
-
.dsh-np-btn:hover{background:var(--dsw-alias-interactive-bg-hover,rgb(38 49 72/.06));color:var(--dsw-alias-label-primary,#1f2430)}
|
|
242
|
-
.dsh-np-btn[data-
|
|
243
|
-
.dsh-np-btn[data-accent=true]
|
|
244
|
-
.dsh-np-btn:disabled{
|
|
245
|
-
.dsh-np-
|
|
246
|
-
.dsh-np-
|
|
247
|
-
.dsh-np-
|
|
248
|
-
.dsh-np-
|
|
249
|
-
.dsh-np-
|
|
250
|
-
.dsh-np-
|
|
251
|
-
.dsh-np-
|
|
252
|
-
.dsh-np-
|
|
253
|
-
.dsh-np-
|
|
254
|
-
.dsh-np-
|
|
255
|
-
.dsh-np-
|
|
256
|
-
.dsh-np-
|
|
257
|
-
.dsh-np-
|
|
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)}
|
|
258
538
|
@keyframes dsh-np-pulse{0%,100%{opacity:1}50%{opacity:.35}}
|
|
259
539
|
@keyframes dsh-np-rise{from{opacity:0;transform:translate(-50%,4px)}to{opacity:1;transform:translate(-50%,0)}}
|
|
260
|
-
@
|
|
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}}`;
|
|
261
546
|
|
|
262
547
|
// ------------------------------------------------------------------
|
|
263
548
|
// Helpers
|
|
264
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
|
+
|
|
265
621
|
function workspaceName(root) {
|
|
266
622
|
const parts = String(root ?? '').split(/[\\/]+/u).filter(Boolean);
|
|
267
623
|
return parts.length === 0 ? String(root ?? '') : parts[parts.length - 1];
|
|
268
624
|
}
|
|
269
625
|
|
|
270
626
|
function readStoredUi() {
|
|
271
|
-
const base = {
|
|
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
|
+
};
|
|
272
636
|
try {
|
|
273
637
|
const raw = window.localStorage?.getItem(UI_KEY);
|
|
274
638
|
if (typeof raw !== 'string' || raw === '') return base;
|
|
@@ -277,8 +641,11 @@ window.__ModuleLoader__.load({
|
|
|
277
641
|
return {
|
|
278
642
|
x: Number.isFinite(parsed.x) ? parsed.x : null,
|
|
279
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,
|
|
280
646
|
minimized: parsed.minimized === true,
|
|
281
647
|
hidden: parsed.hidden === true,
|
|
648
|
+
showArchive: parsed.showArchive === true,
|
|
282
649
|
};
|
|
283
650
|
} catch {
|
|
284
651
|
return base;
|
|
@@ -288,15 +655,18 @@ window.__ModuleLoader__.load({
|
|
|
288
655
|
function clampUi(ui) {
|
|
289
656
|
const vw = (typeof window !== 'undefined' && window.innerWidth) || 1280;
|
|
290
657
|
const vh = (typeof window !== 'undefined' && window.innerHeight) || 800;
|
|
291
|
-
const
|
|
292
|
-
const
|
|
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;
|
|
293
663
|
const x = ui.x === null || ui.x === undefined
|
|
294
664
|
? vw - width - 24
|
|
295
665
|
: Math.min(Math.max(8, ui.x), Math.max(8, vw - width - 8));
|
|
296
666
|
const y = ui.y === null || ui.y === undefined
|
|
297
|
-
? vh -
|
|
298
|
-
: Math.min(Math.max(8, ui.y), Math.max(8, vh -
|
|
299
|
-
return { ...ui, x, y };
|
|
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 };
|
|
300
670
|
}
|
|
301
671
|
|
|
302
672
|
async function readResponse(response) {
|
|
@@ -321,11 +691,11 @@ window.__ModuleLoader__.load({
|
|
|
321
691
|
return readResponse(response);
|
|
322
692
|
}
|
|
323
693
|
|
|
324
|
-
async function saveNote(root, sessionId,
|
|
694
|
+
async function saveNote(root, sessionId, segments) {
|
|
325
695
|
const response = await fetch(`${API}/note`, {
|
|
326
696
|
method: 'PUT',
|
|
327
697
|
headers: { 'Content-Type': 'application/json' },
|
|
328
|
-
body: JSON.stringify({ root, sessionId,
|
|
698
|
+
body: JSON.stringify({ root, sessionId, segments }),
|
|
329
699
|
});
|
|
330
700
|
return readResponse(response);
|
|
331
701
|
}
|
|
@@ -390,17 +760,20 @@ window.__ModuleLoader__.load({
|
|
|
390
760
|
function NotepadOverlay({ ctx, t }) {
|
|
391
761
|
const [binding, setBinding] = useState(null);
|
|
392
762
|
const [present, setPresent] = useState(presentConversation);
|
|
393
|
-
const [
|
|
394
|
-
const [
|
|
763
|
+
const [segments, setSegments] = useState([]);
|
|
764
|
+
const [selected, setSelected] = useState(() => new Set());
|
|
395
765
|
const [status, setStatus] = useState('idle');
|
|
396
766
|
const [error, setError] = useState('');
|
|
397
767
|
const [savedAt, setSavedAt] = useState(null);
|
|
398
768
|
const [toast, setToast] = useState(null);
|
|
399
769
|
const [ui, setUi] = useState(readStoredUi);
|
|
770
|
+
const [sending, setSending] = useState(false);
|
|
771
|
+
const [resizing, setResizing] = useState(false);
|
|
400
772
|
|
|
401
773
|
const bindingRef = useRef(null);
|
|
402
774
|
const rootSessionRef = useRef('');
|
|
403
|
-
const
|
|
775
|
+
const segmentsRef = useRef([]);
|
|
776
|
+
const selectedRef = useRef(selected);
|
|
404
777
|
const rootRef = useRef('');
|
|
405
778
|
const versionRef = useRef(null);
|
|
406
779
|
const dirtyRef = useRef(false);
|
|
@@ -408,9 +781,34 @@ window.__ModuleLoader__.load({
|
|
|
408
781
|
const toastTimerRef = useRef(0);
|
|
409
782
|
const seqRef = useRef(0);
|
|
410
783
|
const saveNowRef = useRef(null);
|
|
784
|
+
const reloadRef = useRef(null);
|
|
785
|
+
const loadFailedRef = useRef(false);
|
|
786
|
+
const loadSeqRef = useRef(0);
|
|
411
787
|
const inputActionsRef = useRef(null);
|
|
412
|
-
const taRef = useRef(null);
|
|
413
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
|
+
};
|
|
414
812
|
|
|
415
813
|
// Session + workspace -------------------------------------------------
|
|
416
814
|
useEffect(() => {
|
|
@@ -458,7 +856,7 @@ window.__ModuleLoader__.load({
|
|
|
458
856
|
return () => clearInterval(timer);
|
|
459
857
|
}, []);
|
|
460
858
|
|
|
461
|
-
// Position
|
|
859
|
+
// Position + size ----------------------------------------------------
|
|
462
860
|
useEffect(() => {
|
|
463
861
|
setUi((previous) => clampUi(previous));
|
|
464
862
|
const onResize = () => setUi((previous) => clampUi(previous));
|
|
@@ -482,10 +880,10 @@ window.__ModuleLoader__.load({
|
|
|
482
880
|
}
|
|
483
881
|
};
|
|
484
882
|
|
|
485
|
-
const showToast = (message) => {
|
|
486
|
-
setToast({ id: Date.now(), message });
|
|
883
|
+
const showToast = (message, action) => {
|
|
884
|
+
setToast({ id: Date.now(), message, action: action ?? null });
|
|
487
885
|
if (toastTimerRef.current) clearTimeout(toastTimerRef.current);
|
|
488
|
-
toastTimerRef.current = setTimeout(() => setToast(null), 1800);
|
|
886
|
+
toastTimerRef.current = setTimeout(() => setToast(null), action === undefined || action === null ? 1800 : 5000);
|
|
489
887
|
};
|
|
490
888
|
|
|
491
889
|
const scheduleSave = () => {
|
|
@@ -502,7 +900,8 @@ window.__ModuleLoader__.load({
|
|
|
502
900
|
const sessionId = rootSessionRef.current;
|
|
503
901
|
if (root === '' || sessionId === '') return;
|
|
504
902
|
if (!dirtyRef.current && options.force !== true) return;
|
|
505
|
-
const body =
|
|
903
|
+
const body = serializeSegments(segmentsRef.current);
|
|
904
|
+
const signature = JSON.stringify(body);
|
|
506
905
|
const seq = ++seqRef.current;
|
|
507
906
|
dirtyRef.current = false;
|
|
508
907
|
setStatus('saving');
|
|
@@ -512,7 +911,7 @@ window.__ModuleLoader__.load({
|
|
|
512
911
|
if (seq !== seqRef.current || rootRef.current !== root) return;
|
|
513
912
|
if (typeof data.mtimeMs === 'number') versionRef.current = data.mtimeMs;
|
|
514
913
|
setSavedAt(typeof data.mtimeMs === 'number' ? data.mtimeMs : Date.now());
|
|
515
|
-
if (
|
|
914
|
+
if (JSON.stringify(serializeSegments(segmentsRef.current)) === signature) {
|
|
516
915
|
dirtyRef.current = false;
|
|
517
916
|
setStatus('saved');
|
|
518
917
|
} else {
|
|
@@ -529,12 +928,25 @@ window.__ModuleLoader__.load({
|
|
|
529
928
|
};
|
|
530
929
|
saveNowRef.current = saveNow;
|
|
531
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
|
+
|
|
532
942
|
// Load on workspace/session change ------------------------------------
|
|
533
943
|
useEffect(() => {
|
|
534
944
|
if (binding === null) {
|
|
535
945
|
rootRef.current = '';
|
|
536
946
|
rootSessionRef.current = '';
|
|
537
|
-
|
|
947
|
+
segmentsRef.current = [];
|
|
948
|
+
setSegments([]);
|
|
949
|
+
setSelection(new Set());
|
|
538
950
|
setStatus('idle');
|
|
539
951
|
setError('');
|
|
540
952
|
return undefined;
|
|
@@ -547,34 +959,76 @@ window.__ModuleLoader__.load({
|
|
|
547
959
|
rootRef.current = root;
|
|
548
960
|
rootSessionRef.current = binding.id;
|
|
549
961
|
let cancelled = false;
|
|
550
|
-
|
|
962
|
+
loadFailedRef.current = false;
|
|
963
|
+
segmentsRef.current = [];
|
|
964
|
+
setSegments([]);
|
|
965
|
+
setSelection(new Set());
|
|
551
966
|
setStatus('loading');
|
|
552
967
|
setError('');
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
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();
|
|
573
995
|
return () => {
|
|
574
996
|
cancelled = true;
|
|
997
|
+
if (reloadRef.current === load) reloadRef.current = null;
|
|
575
998
|
};
|
|
576
999
|
}, [binding === null ? '' : binding.id, binding === null ? '' : binding.cwd]);
|
|
577
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
|
+
|
|
578
1032
|
// Keyboard ------------------------------------------------------------
|
|
579
1033
|
useEffect(() => {
|
|
580
1034
|
const onKeyDown = (event) => {
|
|
@@ -620,14 +1074,18 @@ window.__ModuleLoader__.load({
|
|
|
620
1074
|
const root = rootRef.current;
|
|
621
1075
|
const sessionId = rootSessionRef.current;
|
|
622
1076
|
if (root === '' || sessionId === '' || dirtyRef.current || timerRef.current) return;
|
|
623
|
-
|
|
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;
|
|
624
1080
|
fetchNote(root, sessionId)
|
|
625
1081
|
.then((data) => {
|
|
626
1082
|
if (rootRef.current !== root || dirtyRef.current) return;
|
|
627
1083
|
if (typeof data.mtimeMs === 'number' && data.mtimeMs === versionRef.current) return;
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
1084
|
+
let value = normalizeServerSegments(data);
|
|
1085
|
+
if (value.length === 0) value = [newSegment('')];
|
|
1086
|
+
segmentsRef.current = value;
|
|
1087
|
+
setSegments(value);
|
|
1088
|
+
setSelection(new Set());
|
|
631
1089
|
versionRef.current = typeof data.mtimeMs === 'number' ? data.mtimeMs : null;
|
|
632
1090
|
setSavedAt(typeof data.mtimeMs === 'number' ? data.mtimeMs : null);
|
|
633
1091
|
setStatus('saved');
|
|
@@ -649,47 +1107,277 @@ window.__ModuleLoader__.load({
|
|
|
649
1107
|
};
|
|
650
1108
|
}, []);
|
|
651
1109
|
|
|
652
|
-
//
|
|
653
|
-
const
|
|
654
|
-
const
|
|
655
|
-
|
|
656
|
-
|
|
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);
|
|
657
1115
|
dirtyRef.current = true;
|
|
658
1116
|
setStatus('dirty');
|
|
659
1117
|
setError('');
|
|
660
1118
|
scheduleSave();
|
|
661
1119
|
};
|
|
662
1120
|
|
|
663
|
-
const
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
showToast(t('insertEmpty'));
|
|
1121
|
+
const addSegment = () => {
|
|
1122
|
+
if (segmentsRef.current.length >= MAX_SEGMENTS) {
|
|
1123
|
+
showToast(t('tooManySegments', { n: MAX_SEGMENTS }));
|
|
667
1124
|
return;
|
|
668
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) => {
|
|
669
1235
|
const actions = inputActionsRef.current;
|
|
670
|
-
if (
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
const span = actions.captureInsertion();
|
|
678
|
-
if (actions.insertText(value, span) === true) {
|
|
679
|
-
actions.persistDraft?.();
|
|
680
|
-
showToast(t('inserted'));
|
|
681
|
-
return;
|
|
682
|
-
}
|
|
683
|
-
} catch {
|
|
684
|
-
/* fall through to copy */
|
|
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;
|
|
685
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;
|
|
686
1271
|
}
|
|
687
1272
|
const copied = await copyText(value);
|
|
688
1273
|
showToast(copied ? t('insertUnavailable') : t('copyFailed'));
|
|
689
1274
|
};
|
|
690
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
|
+
|
|
691
1379
|
const copyAll = async () => {
|
|
692
|
-
const value =
|
|
1380
|
+
const value = mergedText(segmentsRef.current);
|
|
693
1381
|
if (value === '') {
|
|
694
1382
|
showToast(t('insertEmpty'));
|
|
695
1383
|
return;
|
|
@@ -697,7 +1385,8 @@ window.__ModuleLoader__.load({
|
|
|
697
1385
|
showToast((await copyText(value)) ? t('copied') : t('copyFailed'));
|
|
698
1386
|
};
|
|
699
1387
|
|
|
700
|
-
|
|
1388
|
+
// UI actions ----------------------------------------------------------
|
|
1389
|
+
const toggleCollapse = () => {
|
|
701
1390
|
setUi((previous) => clampUi({ ...previous, minimized: !previous.minimized }));
|
|
702
1391
|
};
|
|
703
1392
|
|
|
@@ -705,11 +1394,40 @@ window.__ModuleLoader__.load({
|
|
|
705
1394
|
setUi((previous) => clampUi({ ...previous, hidden: !previous.hidden }));
|
|
706
1395
|
};
|
|
707
1396
|
|
|
708
|
-
const
|
|
709
|
-
|
|
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') {
|
|
710
1418
|
event.preventDefault();
|
|
711
|
-
|
|
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;
|
|
712
1427
|
}
|
|
1428
|
+
if (event.key !== 'Enter' || event.shiftKey || event.altKey || chord) return;
|
|
1429
|
+
event.preventDefault();
|
|
1430
|
+
splitSegment(id, event.currentTarget ?? event.target);
|
|
713
1431
|
};
|
|
714
1432
|
|
|
715
1433
|
const onHeaderPointerDown = (event) => {
|
|
@@ -722,9 +1440,8 @@ window.__ModuleLoader__.load({
|
|
|
722
1440
|
const move = (moveEvent) => {
|
|
723
1441
|
const vw = window.innerWidth || 1280;
|
|
724
1442
|
const vh = window.innerHeight || 800;
|
|
725
|
-
const
|
|
726
|
-
const
|
|
727
|
-
const x = Math.min(Math.max(8, start.x + moveEvent.clientX - start.px), Math.max(8, vw - width - 8));
|
|
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));
|
|
728
1445
|
const y = Math.min(Math.max(8, start.y + moveEvent.clientY - start.py), Math.max(8, vh - height - 8));
|
|
729
1446
|
setUi((previous) => ({ ...previous, x, y }));
|
|
730
1447
|
};
|
|
@@ -740,25 +1457,56 @@ window.__ModuleLoader__.load({
|
|
|
740
1457
|
event.preventDefault?.();
|
|
741
1458
|
};
|
|
742
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
|
+
|
|
743
1497
|
// Render --------------------------------------------------------------
|
|
744
1498
|
if (binding === null || present !== true) return null;
|
|
745
1499
|
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
h(NotepadIcon, { size: 16 }),
|
|
757
|
-
);
|
|
758
|
-
}
|
|
759
|
-
|
|
760
|
-
const fallbackX = Math.max(8, ((typeof window !== 'undefined' && window.innerWidth) || 1280) - CARD_W - 24);
|
|
761
|
-
const fallbackY = Math.max(8, ((typeof window !== 'undefined' && window.innerHeight) || 800) - CARD_H - 24);
|
|
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);
|
|
762
1510
|
const left = typeof ui.x === 'number' ? ui.x : fallbackX;
|
|
763
1511
|
const top = typeof ui.y === 'number' ? ui.y : fallbackY;
|
|
764
1512
|
|
|
@@ -766,119 +1514,496 @@ window.__ModuleLoader__.load({
|
|
|
766
1514
|
if (status === 'loading') statusText = t('loading');
|
|
767
1515
|
else if (status === 'saving') statusText = t('saving');
|
|
768
1516
|
else if (status === 'dirty') statusText = t('unsaved');
|
|
769
|
-
else if (status === 'error') statusText = t('saveFailed');
|
|
1517
|
+
else if (status === 'error') statusText = loadFailedRef.current ? t('loadFailed') : t('saveFailed');
|
|
770
1518
|
else if (status === 'saved') {
|
|
771
1519
|
const clock = clockLabel(savedAt);
|
|
772
1520
|
statusText = clock === '' ? t('saved') : `${t('saved')} · ${clock}`;
|
|
773
1521
|
} else statusText = t('empty');
|
|
774
1522
|
|
|
775
|
-
|
|
776
|
-
'
|
|
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',
|
|
777
1682
|
{
|
|
778
|
-
|
|
779
|
-
'
|
|
780
|
-
'data-
|
|
781
|
-
'data-
|
|
782
|
-
|
|
783
|
-
role: 'complementary',
|
|
784
|
-
'aria-label': t('title'),
|
|
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',
|
|
785
1688
|
},
|
|
786
1689
|
h(
|
|
787
1690
|
'div',
|
|
788
|
-
{ className: 'dsh-np-head'
|
|
789
|
-
h(
|
|
790
|
-
|
|
791
|
-
|
|
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')),
|
|
792
1703
|
h('span', { className: 'dsh-np-spacer' }),
|
|
793
1704
|
h(
|
|
794
1705
|
'button',
|
|
795
1706
|
{
|
|
796
1707
|
type: 'button',
|
|
797
|
-
className: 'dsh-np-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
onClick: insertIntoComposer,
|
|
1708
|
+
className: 'dsh-np-segbtn',
|
|
1709
|
+
title: t('insertSegment'),
|
|
1710
|
+
'aria-label': t('insertSegment'),
|
|
1711
|
+
disabled: segment.text.trim() === '',
|
|
1712
|
+
onClick: () => insertSegments([segment]),
|
|
803
1713
|
},
|
|
804
|
-
h(InsertIcon, { size:
|
|
1714
|
+
h(InsertIcon, { size: 14 }),
|
|
805
1715
|
),
|
|
806
1716
|
h(
|
|
807
1717
|
'button',
|
|
808
1718
|
{
|
|
809
1719
|
type: 'button',
|
|
810
|
-
className: 'dsh-np-
|
|
811
|
-
title: t('
|
|
812
|
-
'aria-label': t('
|
|
813
|
-
disabled: text === '',
|
|
814
|
-
onClick:
|
|
1720
|
+
className: 'dsh-np-segbtn',
|
|
1721
|
+
title: t('replaceSegment'),
|
|
1722
|
+
'aria-label': t('replaceSegment'),
|
|
1723
|
+
disabled: segment.text.trim() === '',
|
|
1724
|
+
onClick: () => replaceSegments([segment]),
|
|
815
1725
|
},
|
|
816
|
-
h(
|
|
1726
|
+
h(ReplaceIcon, { size: 14 }),
|
|
817
1727
|
),
|
|
818
1728
|
h(
|
|
819
1729
|
'button',
|
|
820
1730
|
{
|
|
821
1731
|
type: 'button',
|
|
822
|
-
className: 'dsh-np-
|
|
823
|
-
title:
|
|
824
|
-
'aria-label':
|
|
825
|
-
|
|
1732
|
+
className: 'dsh-np-segbtn',
|
|
1733
|
+
title: t('sendSegment'),
|
|
1734
|
+
'aria-label': t('sendSegment'),
|
|
1735
|
+
disabled: segment.text.trim() === '' || sending,
|
|
1736
|
+
onClick: () => sendSegments([segment]),
|
|
826
1737
|
},
|
|
827
|
-
h(
|
|
1738
|
+
h(SendIcon, { size: 14 }),
|
|
828
1739
|
),
|
|
829
1740
|
h(
|
|
830
1741
|
'button',
|
|
831
1742
|
{
|
|
832
1743
|
type: 'button',
|
|
833
|
-
className: 'dsh-np-
|
|
834
|
-
title: t('
|
|
835
|
-
'aria-label': t('
|
|
836
|
-
onClick:
|
|
1744
|
+
className: 'dsh-np-segbtn',
|
|
1745
|
+
title: t('archiveSegment'),
|
|
1746
|
+
'aria-label': t('archiveSegment'),
|
|
1747
|
+
onClick: () => archiveSegment(segment.id),
|
|
837
1748
|
},
|
|
838
|
-
h(
|
|
1749
|
+
h(ArchiveIcon, { size: 14 }),
|
|
839
1750
|
),
|
|
840
1751
|
),
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
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(
|
|
844
1800
|
'div',
|
|
845
|
-
{ className: 'dsh-np-
|
|
846
|
-
h('textarea', {
|
|
847
|
-
ref: taRef,
|
|
848
|
-
className: 'dsh-np-text',
|
|
849
|
-
value: text,
|
|
850
|
-
placeholder: t('placeholder'),
|
|
851
|
-
spellCheck: false,
|
|
852
|
-
maxLength: 160000,
|
|
853
|
-
'aria-label': t('editor'),
|
|
854
|
-
disabled: status === 'loading',
|
|
855
|
-
onChange,
|
|
856
|
-
onKeyDown: onTextKeyDown,
|
|
857
|
-
onBlur: () => {
|
|
858
|
-
if (dirtyRef.current) saveNowRef.current?.();
|
|
859
|
-
},
|
|
860
|
-
}),
|
|
1801
|
+
{ className: 'dsh-np-archive-inner' },
|
|
861
1802
|
h(
|
|
862
1803
|
'div',
|
|
863
|
-
{ className: 'dsh-np-
|
|
864
|
-
h(
|
|
865
|
-
|
|
866
|
-
'
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
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
|
|
870
1818
|
? h('button', {
|
|
871
1819
|
type: 'button',
|
|
872
|
-
className: 'dsh-np-
|
|
873
|
-
|
|
874
|
-
|
|
1820
|
+
className: 'dsh-np-link',
|
|
1821
|
+
'data-danger': 'true',
|
|
1822
|
+
title: t('clearArchive'),
|
|
1823
|
+
onClick: clearArchive,
|
|
1824
|
+
}, t('clearArchive'))
|
|
875
1825
|
: null,
|
|
876
|
-
h('span', { className: 'dsh-np-count' }, t('chars', { n: text.length })),
|
|
877
|
-
h('span', { className: 'dsh-np-hint', title: t('hint') }, t('hint')),
|
|
878
1826
|
),
|
|
879
|
-
|
|
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
|
+
),
|
|
880
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')),
|
|
881
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];
|
|
882
2007
|
}
|
|
883
2008
|
|
|
884
2009
|
// ------------------------------------------------------------------
|