@restregular/dsh-notepad 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +82 -22
- package/client.js +1875 -243
- package/index.js +53 -9
- package/lib/image-store.js +176 -0
- package/lib/note-store.js +288 -25
- package/locale/en.json +1 -1
- package/locale/zh.json +1 -1
- package/package.json +4 -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,22 @@ 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;
|
|
53
|
+
const MAX_IMAGES = 20;
|
|
54
|
+
const MAX_IMAGE_BYTES = 20 * 1024 * 1024;
|
|
55
|
+
const IMAGE_MIME = {
|
|
56
|
+
'image/png': 'png',
|
|
57
|
+
'image/jpeg': 'jpg',
|
|
58
|
+
'image/webp': 'webp',
|
|
59
|
+
'image/gif': 'gif',
|
|
60
|
+
};
|
|
40
61
|
|
|
41
62
|
// ------------------------------------------------------------------
|
|
42
63
|
// Copy
|
|
@@ -44,55 +65,173 @@ window.__ModuleLoader__.load({
|
|
|
44
65
|
const DICT = {
|
|
45
66
|
zh: {
|
|
46
67
|
title: '记事本',
|
|
47
|
-
editor: '
|
|
68
|
+
editor: '分段内容',
|
|
48
69
|
placeholder: '记录开发中遇到的问题,或下一阶段要做的事……',
|
|
70
|
+
segmentPlaceholder: '新分段…',
|
|
49
71
|
loading: '读取中…',
|
|
50
72
|
empty: '还没有内容',
|
|
51
73
|
unsaved: '未保存',
|
|
52
74
|
saving: '保存中…',
|
|
53
75
|
saved: '已保存',
|
|
54
76
|
saveFailed: '保存失败',
|
|
77
|
+
loadFailed: '读取失败',
|
|
55
78
|
retry: '重试',
|
|
56
79
|
insert: '插入到输入框',
|
|
80
|
+
insertSegment: '插入此段',
|
|
81
|
+
insertSelected: '插入选中的 {n} 段',
|
|
57
82
|
inserted: '已插入输入框',
|
|
58
|
-
insertEmpty: '
|
|
83
|
+
insertEmpty: '没有可插入的内容',
|
|
59
84
|
insertUnavailable: '输入框暂不可用,内容已复制',
|
|
85
|
+
replace: '替换输入框内容',
|
|
86
|
+
replaceSegment: '替换此段',
|
|
87
|
+
replaceSelected: '替换为选中的 {n} 段',
|
|
88
|
+
replaced: '已替换输入框内容',
|
|
89
|
+
replaceUnavailable: '替换不可用,内容已复制',
|
|
90
|
+
send: '发送',
|
|
91
|
+
sendSegment: '发送此段',
|
|
92
|
+
sendSelected: '发送选中的 {n} 段',
|
|
93
|
+
sending: '发送中…',
|
|
94
|
+
sent: '已发送',
|
|
95
|
+
sendFailed: '发送失败',
|
|
96
|
+
sendUnavailable: '发送接口不可用,已插入输入框',
|
|
60
97
|
copy: '复制全部',
|
|
61
98
|
copied: '已复制',
|
|
62
99
|
copyFailed: '复制失败',
|
|
63
|
-
|
|
100
|
+
selectAll: '全选',
|
|
101
|
+
clearSelection: '取消选择',
|
|
102
|
+
selectHint: '勾选分段可批量操作',
|
|
103
|
+
selected: '已选 {n} 段',
|
|
104
|
+
selectSegment: '选择此段',
|
|
105
|
+
archive: '归档',
|
|
106
|
+
archiveSegment: '归档此段',
|
|
107
|
+
archiveSelected: '归档选中的 {n} 段',
|
|
108
|
+
archived: '归档',
|
|
109
|
+
archivedToast: '已归档,可在归档中查看',
|
|
110
|
+
archivedCount: '已归档 {n} 段',
|
|
111
|
+
showArchive: '显示归档({n})',
|
|
112
|
+
hideArchive: '隐藏归档({n})',
|
|
113
|
+
archiveCount: '归档 · {n}',
|
|
114
|
+
archiveEmpty: '还没有归档的分段',
|
|
115
|
+
noActive: '没有进行中的分段',
|
|
116
|
+
restore: '恢复',
|
|
117
|
+
restoreSegment: '恢复此段',
|
|
118
|
+
restoreAll: '全部恢复',
|
|
119
|
+
restoredToast: '已恢复',
|
|
120
|
+
restoredCount: '已恢复 {n} 段',
|
|
121
|
+
deleteSegment: '删除此段',
|
|
122
|
+
deletedToast: '已删除',
|
|
123
|
+
clearArchive: '清空归档',
|
|
124
|
+
clearedArchive: '已清空 {n} 段归档',
|
|
125
|
+
undo: '撤销',
|
|
126
|
+
addSegment: '新建分段',
|
|
127
|
+
tooManySegments: '最多 {n} 段',
|
|
128
|
+
emptySegment: '(空分段)',
|
|
129
|
+
collapse: '折叠',
|
|
64
130
|
expand: '展开',
|
|
65
|
-
|
|
131
|
+
minimize: '最小化',
|
|
66
132
|
show: '显示记事本(Ctrl+Shift+N)',
|
|
67
133
|
chars: '{n} 字符',
|
|
68
|
-
|
|
134
|
+
segments: '{n} 段',
|
|
135
|
+
hint: 'Enter 新建分段 · Shift+Enter 换行 · Ctrl+Enter 插入 · 支持粘贴图片',
|
|
69
136
|
workspace: '工作区',
|
|
137
|
+
resize: '拖动调整大小',
|
|
138
|
+
addImage: '添加图片',
|
|
139
|
+
removeImage: '移除图片',
|
|
140
|
+
openImage: '打开图片',
|
|
141
|
+
imageAdded: '已添加 {n} 张图片',
|
|
142
|
+
imageFailed: '图片添加失败',
|
|
143
|
+
imageUnsupported: '只支持 PNG / JPEG / WebP / GIF 图片',
|
|
144
|
+
imageTooLarge: '图片太大(最大 20 MB)',
|
|
145
|
+
imageTooMany: '每段最多 {n} 张图片',
|
|
146
|
+
imageUploading: '图片处理中…',
|
|
147
|
+
images: '{n} 张图片',
|
|
148
|
+
dropImage: '松开以添加图片',
|
|
149
|
+
imagePasteSaved: '已保存到 .dsh/notepad/images',
|
|
70
150
|
},
|
|
71
151
|
en: {
|
|
72
152
|
title: 'Notepad',
|
|
73
|
-
editor: '
|
|
153
|
+
editor: 'Segment content',
|
|
74
154
|
placeholder: 'Jot down problems, or what to build next…',
|
|
155
|
+
segmentPlaceholder: 'New segment…',
|
|
75
156
|
loading: 'Loading…',
|
|
76
157
|
empty: 'Nothing here yet',
|
|
77
158
|
unsaved: 'Unsaved',
|
|
78
159
|
saving: 'Saving…',
|
|
79
160
|
saved: 'Saved',
|
|
80
161
|
saveFailed: 'Save failed',
|
|
162
|
+
loadFailed: 'Load failed',
|
|
81
163
|
retry: 'Retry',
|
|
82
164
|
insert: 'Insert into composer',
|
|
83
|
-
|
|
84
|
-
|
|
165
|
+
insertSegment: 'Insert this segment',
|
|
166
|
+
insertSelected: 'Insert {n} selected',
|
|
167
|
+
inserted: 'Inserted into composer',
|
|
168
|
+
insertEmpty: 'Nothing to insert',
|
|
85
169
|
insertUnavailable: 'Composer unavailable; copied instead',
|
|
170
|
+
replace: 'Replace composer',
|
|
171
|
+
replaceSegment: 'Replace with this segment',
|
|
172
|
+
replaceSelected: 'Replace with {n} selected',
|
|
173
|
+
replaced: 'Composer replaced',
|
|
174
|
+
replaceUnavailable: 'Replace unavailable; copied instead',
|
|
175
|
+
send: 'Send',
|
|
176
|
+
sendSegment: 'Send this segment',
|
|
177
|
+
sendSelected: 'Send {n} selected',
|
|
178
|
+
sending: 'Sending…',
|
|
179
|
+
sent: 'Sent',
|
|
180
|
+
sendFailed: 'Send failed',
|
|
181
|
+
sendUnavailable: 'Send unavailable; inserted into composer',
|
|
86
182
|
copy: 'Copy all',
|
|
87
183
|
copied: 'Copied',
|
|
88
184
|
copyFailed: 'Copy failed',
|
|
89
|
-
|
|
185
|
+
selectAll: 'Select all',
|
|
186
|
+
clearSelection: 'Clear selection',
|
|
187
|
+
selectHint: 'Check segments to batch',
|
|
188
|
+
selected: '{n} selected',
|
|
189
|
+
selectSegment: 'Select this segment',
|
|
190
|
+
archive: 'Archive',
|
|
191
|
+
archiveSegment: 'Archive this segment',
|
|
192
|
+
archiveSelected: 'Archive {n} selected',
|
|
193
|
+
archived: 'Archived',
|
|
194
|
+
archivedToast: 'Archived; available under Archive',
|
|
195
|
+
archivedCount: 'Archived {n}',
|
|
196
|
+
showArchive: 'Show archive ({n})',
|
|
197
|
+
hideArchive: 'Hide archive ({n})',
|
|
198
|
+
archiveCount: 'Archive · {n}',
|
|
199
|
+
archiveEmpty: 'Nothing archived yet',
|
|
200
|
+
noActive: 'No active segments',
|
|
201
|
+
restore: 'Restore',
|
|
202
|
+
restoreSegment: 'Restore this segment',
|
|
203
|
+
restoreAll: 'Restore all',
|
|
204
|
+
restoredToast: 'Restored',
|
|
205
|
+
restoredCount: 'Restored {n}',
|
|
206
|
+
deleteSegment: 'Delete this segment',
|
|
207
|
+
deletedToast: 'Deleted',
|
|
208
|
+
clearArchive: 'Clear archive',
|
|
209
|
+
clearedArchive: 'Cleared {n} archived',
|
|
210
|
+
undo: 'Undo',
|
|
211
|
+
addSegment: 'New segment',
|
|
212
|
+
tooManySegments: 'At most {n} segments',
|
|
213
|
+
emptySegment: '(empty segment)',
|
|
214
|
+
collapse: 'Collapse',
|
|
90
215
|
expand: 'Expand',
|
|
91
|
-
|
|
216
|
+
minimize: 'Minimize',
|
|
92
217
|
show: 'Show notepad (Ctrl+Shift+N)',
|
|
93
218
|
chars: '{n} chars',
|
|
94
|
-
|
|
219
|
+
segments: '{n} segments',
|
|
220
|
+
hint: 'Enter new segment · Shift+Enter newline · Ctrl+Enter insert · Paste images',
|
|
95
221
|
workspace: 'Workspace',
|
|
222
|
+
resize: 'Drag to resize',
|
|
223
|
+
addImage: 'Add image',
|
|
224
|
+
removeImage: 'Remove image',
|
|
225
|
+
openImage: 'Open image',
|
|
226
|
+
imageAdded: 'Added {n} image(s)',
|
|
227
|
+
imageFailed: 'Could not add image',
|
|
228
|
+
imageUnsupported: 'Only PNG / JPEG / WebP / GIF images are supported',
|
|
229
|
+
imageTooLarge: 'Image is too large (20 MB max)',
|
|
230
|
+
imageTooMany: 'At most {n} images per segment',
|
|
231
|
+
imageUploading: 'Processing image…',
|
|
232
|
+
images: '{n} image(s)',
|
|
233
|
+
dropImage: 'Drop to add the image',
|
|
234
|
+
imagePasteSaved: 'Saved under .dsh/notepad/images',
|
|
96
235
|
},
|
|
97
236
|
};
|
|
98
237
|
|
|
@@ -150,18 +289,55 @@ window.__ModuleLoader__.load({
|
|
|
150
289
|
);
|
|
151
290
|
}
|
|
152
291
|
|
|
292
|
+
/** Arrow pointing to the bottom-left corner. */
|
|
153
293
|
function InsertIcon({ size = 16 }) {
|
|
154
294
|
return h(
|
|
155
295
|
Svg,
|
|
156
296
|
{ size, viewBox: '0 0 16 16' },
|
|
157
297
|
h('path', {
|
|
158
|
-
d: '
|
|
298
|
+
d: 'M13.2 2.8 3.6 12.4',
|
|
299
|
+
stroke: 'currentColor', strokeWidth: 1.4, strokeLinecap: 'round',
|
|
300
|
+
}),
|
|
301
|
+
h('path', {
|
|
302
|
+
d: 'M3.4 7.2v5.4h5.4',
|
|
303
|
+
stroke: 'currentColor', strokeWidth: 1.4,
|
|
304
|
+
strokeLinecap: 'round', strokeLinejoin: 'round',
|
|
305
|
+
}),
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/** Two opposing arrows: replace the composer content. */
|
|
310
|
+
function ReplaceIcon({ size = 16 }) {
|
|
311
|
+
return h(
|
|
312
|
+
Svg,
|
|
313
|
+
{ size, viewBox: '0 0 16 16' },
|
|
314
|
+
h('path', {
|
|
315
|
+
d: 'M2.6 5.4h8.2M8.8 3.2l2.2 2.2-2.2 2.2',
|
|
159
316
|
stroke: 'currentColor', strokeWidth: 1.35,
|
|
160
317
|
strokeLinecap: 'round', strokeLinejoin: 'round',
|
|
161
318
|
}),
|
|
162
319
|
h('path', {
|
|
163
|
-
d: '
|
|
164
|
-
stroke: 'currentColor', strokeWidth: 1.35,
|
|
320
|
+
d: 'M13.4 10.6H5.2M7.2 8.4 5 10.6l2.2 2.2',
|
|
321
|
+
stroke: 'currentColor', strokeWidth: 1.35,
|
|
322
|
+
strokeLinecap: 'round', strokeLinejoin: 'round',
|
|
323
|
+
}),
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/** Outline paper plane. */
|
|
328
|
+
function SendIcon({ size = 16 }) {
|
|
329
|
+
return h(
|
|
330
|
+
Svg,
|
|
331
|
+
{ size, viewBox: '0 0 16 16' },
|
|
332
|
+
h('path', {
|
|
333
|
+
d: 'M13.9 2.1 2.3 6.4l4.7 2.3L13.9 2.1Z',
|
|
334
|
+
stroke: 'currentColor', strokeWidth: 1.25,
|
|
335
|
+
strokeLinecap: 'round', strokeLinejoin: 'round',
|
|
336
|
+
}),
|
|
337
|
+
h('path', {
|
|
338
|
+
d: 'M13.9 2.1 9.4 13.7 7 8.7',
|
|
339
|
+
stroke: 'currentColor', strokeWidth: 1.25,
|
|
340
|
+
strokeLinecap: 'round', strokeLinejoin: 'round',
|
|
165
341
|
}),
|
|
166
342
|
);
|
|
167
343
|
}
|
|
@@ -181,94 +357,533 @@ window.__ModuleLoader__.load({
|
|
|
181
357
|
);
|
|
182
358
|
}
|
|
183
359
|
|
|
184
|
-
function
|
|
360
|
+
function ArchiveIcon({ size = 16 }) {
|
|
361
|
+
return h(
|
|
362
|
+
Svg,
|
|
363
|
+
{ size, viewBox: '0 0 16 16' },
|
|
364
|
+
h('path', {
|
|
365
|
+
d: 'M2.2 3.2h11.6v2.8H2.2z',
|
|
366
|
+
stroke: 'currentColor', strokeWidth: 1.25,
|
|
367
|
+
strokeLinejoin: 'round',
|
|
368
|
+
}),
|
|
369
|
+
h('path', {
|
|
370
|
+
d: 'M3.2 6v6.1c0 .7.6 1.3 1.3 1.3h7c.7 0 1.3-.6 1.3-1.3V6',
|
|
371
|
+
stroke: 'currentColor', strokeWidth: 1.25, strokeLinecap: 'round',
|
|
372
|
+
}),
|
|
373
|
+
h('path', {
|
|
374
|
+
d: 'M6.2 8.6h3.6',
|
|
375
|
+
stroke: 'currentColor', strokeWidth: 1.25, strokeLinecap: 'round',
|
|
376
|
+
}),
|
|
377
|
+
);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function RestoreIcon({ size = 16 }) {
|
|
381
|
+
return h(
|
|
382
|
+
Svg,
|
|
383
|
+
{ size, viewBox: '0 0 16 16' },
|
|
384
|
+
h('path', {
|
|
385
|
+
d: 'M8 10.2V3.2M5.6 5.6 8 3.2l2.4 2.4',
|
|
386
|
+
stroke: 'currentColor', strokeWidth: 1.35,
|
|
387
|
+
strokeLinecap: 'round', strokeLinejoin: 'round',
|
|
388
|
+
}),
|
|
389
|
+
h('path', {
|
|
390
|
+
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',
|
|
391
|
+
stroke: 'currentColor', strokeWidth: 1.35, strokeLinecap: 'round',
|
|
392
|
+
}),
|
|
393
|
+
);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function TrashIcon({ size = 16 }) {
|
|
397
|
+
return h(
|
|
398
|
+
Svg,
|
|
399
|
+
{ size, viewBox: '0 0 16 16' },
|
|
400
|
+
h('path', {
|
|
401
|
+
d: 'M2.8 4.4h10.4',
|
|
402
|
+
stroke: 'currentColor', strokeWidth: 1.3, strokeLinecap: 'round',
|
|
403
|
+
}),
|
|
404
|
+
h('path', {
|
|
405
|
+
d: 'M6.1 4.4V3.2c0-.5.4-.9.9-.9h2c.5 0 .9.4.9.9v1.2',
|
|
406
|
+
stroke: 'currentColor', strokeWidth: 1.3, strokeLinecap: 'round',
|
|
407
|
+
}),
|
|
408
|
+
h('path', {
|
|
409
|
+
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',
|
|
410
|
+
stroke: 'currentColor', strokeWidth: 1.3, strokeLinecap: 'round',
|
|
411
|
+
}),
|
|
412
|
+
);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
function PlusIcon({ size = 16 }) {
|
|
185
416
|
return h(
|
|
186
417
|
Svg,
|
|
187
418
|
{ size, viewBox: '0 0 16 16' },
|
|
188
419
|
h('path', {
|
|
189
|
-
d: '
|
|
420
|
+
d: 'M8 3.2v9.6M3.2 8h9.6',
|
|
190
421
|
stroke: 'currentColor', strokeWidth: 1.4, strokeLinecap: 'round',
|
|
191
422
|
}),
|
|
192
423
|
);
|
|
193
424
|
}
|
|
194
425
|
|
|
195
|
-
function
|
|
426
|
+
function CloseIcon({ size = 16 }) {
|
|
196
427
|
return h(
|
|
197
428
|
Svg,
|
|
198
429
|
{ size, viewBox: '0 0 16 16' },
|
|
199
430
|
h('path', {
|
|
200
|
-
d: '
|
|
201
|
-
stroke: 'currentColor', strokeWidth: 1.4,
|
|
431
|
+
d: 'm4.2 4.2 7.6 7.6M11.8 4.2l-7.6 7.6',
|
|
432
|
+
stroke: 'currentColor', strokeWidth: 1.4, strokeLinecap: 'round',
|
|
433
|
+
}),
|
|
434
|
+
);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/** Chevrons folded towards each other (collapse). */
|
|
438
|
+
function ChevronsCollapseIcon({ size = 16 }) {
|
|
439
|
+
return h(
|
|
440
|
+
Svg,
|
|
441
|
+
{ size, viewBox: '0 0 16 16' },
|
|
442
|
+
h('path', {
|
|
443
|
+
d: 'M4.2 5 8 8.8 11.8 5',
|
|
444
|
+
stroke: 'currentColor', strokeWidth: 1.35,
|
|
445
|
+
strokeLinecap: 'round', strokeLinejoin: 'round',
|
|
446
|
+
}),
|
|
447
|
+
h('path', {
|
|
448
|
+
d: 'M4.2 11 8 7.2 11.8 11',
|
|
449
|
+
stroke: 'currentColor', strokeWidth: 1.35,
|
|
202
450
|
strokeLinecap: 'round', strokeLinejoin: 'round',
|
|
203
451
|
}),
|
|
204
452
|
);
|
|
205
453
|
}
|
|
206
454
|
|
|
207
|
-
|
|
455
|
+
/** Chevrons opening away from each other (expand). */
|
|
456
|
+
function ChevronsExpandIcon({ size = 16 }) {
|
|
208
457
|
return h(
|
|
209
458
|
Svg,
|
|
210
459
|
{ size, viewBox: '0 0 16 16' },
|
|
211
460
|
h('path', {
|
|
212
|
-
d: '
|
|
213
|
-
stroke: 'currentColor', strokeWidth: 1.
|
|
461
|
+
d: 'M4.2 8.8 8 5 11.8 8.8',
|
|
462
|
+
stroke: 'currentColor', strokeWidth: 1.35,
|
|
214
463
|
strokeLinecap: 'round', strokeLinejoin: 'round',
|
|
215
464
|
}),
|
|
216
|
-
h('circle', { cx: 8, cy: 8, r: 1.55, stroke: 'currentColor', strokeWidth: 1.25 }),
|
|
217
465
|
h('path', {
|
|
218
|
-
d: '
|
|
466
|
+
d: 'M4.2 7.2 8 11 11.8 7.2',
|
|
467
|
+
stroke: 'currentColor', strokeWidth: 1.35,
|
|
468
|
+
strokeLinecap: 'round', strokeLinejoin: 'round',
|
|
469
|
+
}),
|
|
470
|
+
);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/** Picture frame with a mountain: image attachment. */
|
|
474
|
+
function ImageIcon({ size = 16 }) {
|
|
475
|
+
return h(
|
|
476
|
+
Svg,
|
|
477
|
+
{ size, viewBox: '0 0 16 16' },
|
|
478
|
+
h('rect', {
|
|
479
|
+
x: 2.2, y: 3.2, width: 11.6, height: 9.6, rx: 1.6,
|
|
480
|
+
stroke: 'currentColor', strokeWidth: 1.25, fill: 'none',
|
|
481
|
+
}),
|
|
482
|
+
h('circle', { cx: 5.9, cy: 6.5, r: 1.05, fill: 'currentColor' }),
|
|
483
|
+
h('path', {
|
|
484
|
+
d: 'M3.4 11.5 6.6 8.5l2 1.8 1.9-2.1 2.3 2.4',
|
|
485
|
+
stroke: 'currentColor', strokeWidth: 1.25,
|
|
486
|
+
strokeLinecap: 'round', strokeLinejoin: 'round', fill: 'none',
|
|
487
|
+
}),
|
|
488
|
+
);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/** Arrow towards the bottom-right corner: minimize to the corner icon. */
|
|
492
|
+
function MinimizeIcon({ size = 16 }) {
|
|
493
|
+
return h(
|
|
494
|
+
Svg,
|
|
495
|
+
{ size, viewBox: '0 0 16 16' },
|
|
496
|
+
h('path', {
|
|
497
|
+
d: 'M4.2 4.2 11.4 11.4',
|
|
219
498
|
stroke: 'currentColor', strokeWidth: 1.35, strokeLinecap: 'round',
|
|
220
499
|
}),
|
|
500
|
+
h('path', {
|
|
501
|
+
d: 'M6.8 11.6h4.8V6.8',
|
|
502
|
+
stroke: 'currentColor', strokeWidth: 1.35,
|
|
503
|
+
strokeLinecap: 'round', strokeLinejoin: 'round',
|
|
504
|
+
}),
|
|
221
505
|
);
|
|
222
506
|
}
|
|
223
507
|
|
|
224
508
|
// ------------------------------------------------------------------
|
|
225
509
|
// Styles
|
|
226
510
|
// ------------------------------------------------------------------
|
|
227
|
-
const
|
|
228
|
-
|
|
229
|
-
.dsh-np-
|
|
511
|
+
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)';
|
|
512
|
+
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}
|
|
513
|
+
.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}
|
|
514
|
+
.dsh-np-card[data-resizing=true]{transition:none;user-select:none}
|
|
515
|
+
.dsh-np-card[data-minimized=true]{border-bottom-color:transparent}
|
|
516
|
+
.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
517
|
.dsh-np-card[data-minimized=true] .dsh-np-head{border-bottom-color:transparent}
|
|
231
518
|
.dsh-np-head:active{cursor:grabbing}
|
|
232
519
|
.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
|
|
520
|
+
.dsh-np-dot[data-state=dirty],.dsh-np-dot[data-state=saving]{background:var(--dsw-alias-state-warn-primary,#f5a524)}
|
|
521
|
+
.dsh-np-dot[data-state=saving]{animation:dsh-np-pulse 1s ease-in-out infinite}
|
|
522
|
+
.dsh-np-dot[data-state=error]{background:var(--dsw-alias-state-error-primary,#e25555)}
|
|
523
|
+
.dsh-np-dot[data-state=saved]{background:var(--dsw-alias-state-success-primary,#2fb36d)}
|
|
237
524
|
.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}
|
|
525
|
+
.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
526
|
.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-
|
|
527
|
+
.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}
|
|
528
|
+
.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)}
|
|
529
|
+
.dsh-np-btn[data-active=true]{background:color-mix(in srgb,var(--np-accent) 12%,transparent);color:var(--np-accent)}
|
|
530
|
+
.dsh-np-btn[data-accent=true]{background:var(--np-accent);color:var(--np-accent-fg)}
|
|
531
|
+
.dsh-np-btn[data-accent=true]:hover:not(:disabled){background:color-mix(in srgb,var(--np-accent) 88%,#000);color:var(--np-accent-fg)}
|
|
532
|
+
.dsh-np-btn:disabled{opacity:.38;cursor:default;background:transparent;color:var(--dsw-alias-label-tertiary,#6b7280)}
|
|
533
|
+
.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,.dsh-np-img-open:focus-visible,.dsh-np-img-remove:focus-visible{outline:2px solid var(--dsw-focus-ring-color,var(--dsw-alias-state-business-primary,#4176e6));outline-offset:-2px}
|
|
534
|
+
.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}
|
|
535
|
+
.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}
|
|
536
|
+
.dsh-np-card[data-minimized=true] .dsh-np-body{opacity:0;visibility:hidden;pointer-events:none;transition:opacity .16s ease,visibility 0s linear .16s}
|
|
537
|
+
.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))}
|
|
538
|
+
.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}
|
|
539
|
+
.dsh-np-selectall input{width:14px;height:14px;margin:0;accent-color:var(--np-accent);cursor:pointer;flex:none}
|
|
540
|
+
.dsh-np-selectall input:disabled{cursor:default;opacity:.45}
|
|
541
|
+
.dsh-np-selcount{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:150px}
|
|
542
|
+
.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)}
|
|
543
|
+
.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}
|
|
544
|
+
.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}
|
|
545
|
+
.dsh-np-seg[data-archived=true]{border-style:dashed;background:transparent;opacity:.82;animation:none}
|
|
546
|
+
.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}
|
|
547
|
+
.dsh-np-check{display:inline-flex;align-items:center;justify-content:center;flex:none;width:20px;height:20px;cursor:pointer}
|
|
548
|
+
.dsh-np-check input{width:14px;height:14px;margin:0;accent-color:var(--np-accent);cursor:pointer}
|
|
549
|
+
.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}
|
|
550
|
+
.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}
|
|
551
|
+
.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}
|
|
552
|
+
.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}
|
|
553
|
+
.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))}
|
|
554
|
+
.dsh-np-segbtn:disabled{opacity:.3;cursor:default}
|
|
555
|
+
.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}
|
|
556
|
+
.dsh-np-seg-text::placeholder{color:var(--dsw-alias-label-caption,#9aa3b2)}
|
|
557
|
+
.dsh-np-seg-text:disabled{opacity:.6}
|
|
558
|
+
.dsh-np-file{display:none}
|
|
559
|
+
.dsh-np-editor{display:flex;flex-direction:column;min-width:0;border-radius:0 0 9px 9px;transition:background .12s}
|
|
560
|
+
.dsh-np-editor[data-drag=true]{background:color-mix(in srgb,var(--np-accent) 8%,transparent);box-shadow:inset 0 0 0 1.5px var(--np-accent)}
|
|
561
|
+
.dsh-np-medias{display:flex;flex-wrap:wrap;gap:5px;padding:5px 9px 0}
|
|
562
|
+
.dsh-np-img{display:inline-flex;align-items:center;gap:4px;max-width:100%;height:38px;padding:2px;box-sizing:border-box;border:1px solid var(--dsw-alias-border-l2,rgb(15 23 42/.12));border-radius:8px;background:var(--np-surface);box-shadow:0 1px 2px rgb(15 23 42/.05)}
|
|
563
|
+
.dsh-np-img[data-pending=true]{padding-right:8px;color:var(--dsw-alias-label-tertiary,#6b7280)}
|
|
564
|
+
.dsh-np-img-open{display:inline-flex;align-items:center;justify-content:center;flex:none;width:32px;height:32px;padding:0;border:0;border-radius:6px;background:var(--dsw-alias-interactive-bg-hover,rgb(38 49 72/.06));overflow:hidden;cursor:zoom-in}
|
|
565
|
+
.dsh-np-img-open img{display:block;width:100%;height:100%;object-fit:cover}
|
|
566
|
+
.dsh-np-img-name{max-width:118px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:10.5px;line-height:13px;color:var(--dsw-alias-label-secondary,#4b5563)}
|
|
567
|
+
.dsh-np-img-remove{display:inline-flex;align-items:center;justify-content:center;flex:none;width:17px;height:17px;padding:0;border:0;border-radius:4px;background:transparent;color:var(--dsw-alias-label-caption,#9aa3b2);cursor:pointer}
|
|
568
|
+
.dsh-np-img-remove:hover{background:var(--dsw-alias-interactive-bg-hover,rgb(38 49 72/.06));color:var(--dsw-alias-state-error-primary,#e25555)}
|
|
569
|
+
.dsh-np-img-spinner{flex:none;width:14px;height:14px;border:2px solid color-mix(in srgb,var(--np-accent) 30%,transparent);border-top-color:var(--np-accent);border-radius:50%;animation:dsh-np-spin .8s linear infinite}
|
|
570
|
+
.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}
|
|
571
|
+
.dsh-np-seg[data-archived=true] .dsh-np-medias{padding:0 10px 8px}
|
|
572
|
+
.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}
|
|
573
|
+
.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}
|
|
574
|
+
.dsh-np-empty-add:hover{color:var(--dsw-alias-label-primary,#1f2430);background:var(--dsw-alias-interactive-bg-hover,rgb(38 49 72/.06))}
|
|
575
|
+
.dsh-np-empty-small{padding:8px 2px;color:var(--dsw-alias-label-tertiary,#6b7280);font-size:11px}
|
|
576
|
+
.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}
|
|
577
|
+
.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}
|
|
578
|
+
.dsh-np-archive-inner{display:flex;flex-direction:column;gap:8px;min-height:0;overflow:hidden}
|
|
579
|
+
.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}
|
|
580
|
+
.dsh-np-archive-label{font-weight:600;letter-spacing:.02em}
|
|
581
|
+
.dsh-np-archive-list{display:flex;flex-direction:column;gap:8px;min-height:0}
|
|
582
|
+
.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}
|
|
583
|
+
.dsh-np-link:hover{text-decoration:underline}
|
|
584
|
+
.dsh-np-link[data-danger=true]{color:var(--dsw-alias-state-error-primary,#e25555)}
|
|
585
|
+
.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}
|
|
586
|
+
.dsh-np-status{flex:none;max-width:42%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
587
|
+
.dsh-np-status[data-state=error]{color:var(--dsw-alias-state-error-primary,#d94a4a)}
|
|
588
|
+
.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}
|
|
589
|
+
.dsh-np-count{flex:none;font-variant-numeric:tabular-nums}
|
|
590
|
+
.dsh-np-hint{margin-left:auto;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;opacity:.85;font-size:10px}
|
|
591
|
+
.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}
|
|
592
|
+
.dsh-np-toast-text{overflow:hidden;text-overflow:ellipsis}
|
|
593
|
+
.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}
|
|
594
|
+
.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}
|
|
595
|
+
.dsh-np-resize:hover{opacity:1}
|
|
596
|
+
.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}
|
|
597
|
+
.dsh-np-resize::after{right:7px;bottom:7px;width:4px;height:4px;opacity:.7}
|
|
598
|
+
.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}
|
|
599
|
+
.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}
|
|
600
|
+
.dsh-np-restore[data-visible=true]:hover{opacity:1;transform:translateY(-1px)}
|
|
258
601
|
@keyframes dsh-np-pulse{0%,100%{opacity:1}50%{opacity:.35}}
|
|
259
602
|
@keyframes dsh-np-rise{from{opacity:0;transform:translate(-50%,4px)}to{opacity:1;transform:translate(-50%,0)}}
|
|
260
|
-
@
|
|
603
|
+
@keyframes dsh-np-item-in{from{opacity:0;transform:translateY(-3px)}to{opacity:1;transform:none}}
|
|
604
|
+
@keyframes dsh-np-spin{to{transform:rotate(360deg)}}
|
|
605
|
+
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)}
|
|
606
|
+
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))}
|
|
607
|
+
body[data-ds-dark-theme] .dsh-np-btn[data-accent=true]:hover:not(:disabled){background:color-mix(in srgb,var(--np-accent) 84%,#fff)}
|
|
608
|
+
body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
|
|
609
|
+
@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}.dsh-np-img-spinner{animation:none}}`;
|
|
261
610
|
|
|
262
611
|
// ------------------------------------------------------------------
|
|
263
612
|
// Helpers
|
|
264
613
|
// ------------------------------------------------------------------
|
|
614
|
+
let segmentSequence = 0;
|
|
615
|
+
|
|
616
|
+
function newSegmentId() {
|
|
617
|
+
const cryptoObject = typeof globalThis !== 'undefined' ? globalThis.crypto : undefined;
|
|
618
|
+
if (cryptoObject !== undefined && typeof cryptoObject.randomUUID === 'function') {
|
|
619
|
+
try {
|
|
620
|
+
return cryptoObject.randomUUID();
|
|
621
|
+
} catch {
|
|
622
|
+
/* fall through */
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
segmentSequence += 1;
|
|
626
|
+
return `seg-${Date.now().toString(36)}-${segmentSequence.toString(36)}`;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
function newSegment(text) {
|
|
630
|
+
return { id: newSegmentId(), text, images: [], archived: false, archivedAt: null };
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
function serializeSegments(segments) {
|
|
634
|
+
return segments.map((segment) => ({
|
|
635
|
+
id: segment.id,
|
|
636
|
+
text: segment.text,
|
|
637
|
+
...(Array.isArray(segment.images) && segment.images.length > 0
|
|
638
|
+
? { images: segment.images.map((image) => ({ path: image.path, name: image.name })) }
|
|
639
|
+
: {}),
|
|
640
|
+
...(segment.archived === true ? { archived: true } : {}),
|
|
641
|
+
...(segment.archived === true && typeof segment.archivedAt === 'string' ? { archivedAt: segment.archivedAt } : {}),
|
|
642
|
+
}));
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
function normalizeServerSegments(data) {
|
|
646
|
+
if (Array.isArray(data?.segments)) {
|
|
647
|
+
const seen = new Set();
|
|
648
|
+
const out = [];
|
|
649
|
+
for (const item of data.segments) {
|
|
650
|
+
if (item === null || typeof item !== 'object' || typeof item.text !== 'string') continue;
|
|
651
|
+
let id = typeof item.id === 'string' && item.id !== '' ? item.id : newSegmentId();
|
|
652
|
+
while (seen.has(id)) id = newSegmentId();
|
|
653
|
+
seen.add(id);
|
|
654
|
+
const archived = item.archived === true;
|
|
655
|
+
out.push({
|
|
656
|
+
id,
|
|
657
|
+
text: item.text,
|
|
658
|
+
images: normalizeImages(item.images),
|
|
659
|
+
archived,
|
|
660
|
+
archivedAt: archived && typeof item.archivedAt === 'string' ? item.archivedAt : null,
|
|
661
|
+
});
|
|
662
|
+
}
|
|
663
|
+
return out;
|
|
664
|
+
}
|
|
665
|
+
const text = typeof data?.text === 'string' ? data.text : '';
|
|
666
|
+
return text === '' ? [] : [newSegment(text)];
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
function baseNameOf(value) {
|
|
670
|
+
const trimmed = String(value ?? '').replace(/[/\\]+$/u, '');
|
|
671
|
+
const separator = Math.max(trimmed.lastIndexOf('/'), trimmed.lastIndexOf('\\'));
|
|
672
|
+
return trimmed.slice(separator + 1);
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
function normalizeImage(value) {
|
|
676
|
+
if (typeof value === 'string') value = { path: value };
|
|
677
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) return null;
|
|
678
|
+
if (typeof value.path !== 'string') return null;
|
|
679
|
+
const filePath = value.path.trim().replace(/\\/gu, '/');
|
|
680
|
+
if (filePath === '') return null;
|
|
681
|
+
const rawName = typeof value.name === 'string' && value.name.trim() !== ''
|
|
682
|
+
? value.name.trim().replace(/\s+/gu, ' ')
|
|
683
|
+
: baseNameOf(filePath);
|
|
684
|
+
return { path: filePath, name: rawName === '' ? 'image' : rawName.slice(0, 160) };
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
function normalizeImages(value) {
|
|
688
|
+
if (!Array.isArray(value)) return [];
|
|
689
|
+
const out = [];
|
|
690
|
+
for (const item of value) {
|
|
691
|
+
if (out.length >= MAX_IMAGES) break;
|
|
692
|
+
const image = normalizeImage(item);
|
|
693
|
+
if (image !== null) out.push(image);
|
|
694
|
+
}
|
|
695
|
+
return out;
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
/** Markdown reference used in the note body and composer transfers. */
|
|
699
|
+
function imageMarkdown(image) {
|
|
700
|
+
const file = typeof image?.path === 'string' ? image.path.replace(/\\/gu, '/') : '';
|
|
701
|
+
if (file === '') return '';
|
|
702
|
+
const label = (typeof image?.name === 'string' ? image.name : '')
|
|
703
|
+
.replace(/[\r\n]+/gu, ' ')
|
|
704
|
+
.replace(/[[\]]/gu, ' ')
|
|
705
|
+
.trim() || baseNameOf(file) || 'image';
|
|
706
|
+
let target;
|
|
707
|
+
try {
|
|
708
|
+
target = encodeURI(file);
|
|
709
|
+
} catch {
|
|
710
|
+
target = file;
|
|
711
|
+
}
|
|
712
|
+
target = target
|
|
713
|
+
.replace(/#/gu, '%23')
|
|
714
|
+
.replace(/\?/gu, '%3F')
|
|
715
|
+
.replace(/\(/gu, '%28')
|
|
716
|
+
.replace(/\)/gu, '%29');
|
|
717
|
+
return ``;
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
/** One segment as text plus one Markdown line per attached image. */
|
|
721
|
+
function segmentBody(segment) {
|
|
722
|
+
if (segment === null || segment === undefined) return '';
|
|
723
|
+
const text = typeof segment.text === 'string' ? segment.text : '';
|
|
724
|
+
const images = Array.isArray(segment.images) ? segment.images : [];
|
|
725
|
+
const parts = [];
|
|
726
|
+
if (text.trim() !== '') parts.push(text);
|
|
727
|
+
for (const image of images) {
|
|
728
|
+
const markdown = imageMarkdown(image);
|
|
729
|
+
if (markdown !== '') parts.push(markdown);
|
|
730
|
+
}
|
|
731
|
+
return parts.join('\n');
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
function mergedText(segments) {
|
|
735
|
+
return segments
|
|
736
|
+
.map((segment) => segmentBody(segment))
|
|
737
|
+
.filter((text) => text !== '')
|
|
738
|
+
.join('\n\n');
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
function isAbsolutePath(value) {
|
|
742
|
+
const text = String(value ?? '');
|
|
743
|
+
return text.startsWith('/') || /^[A-Za-z]:\//u.test(text) || text.startsWith('//');
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
function normalizedCwd(value) {
|
|
747
|
+
return String(value ?? '').replace(/\\/gu, '/').replace(/\/+$/u, '');
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
/** Store a path relative to the workspace when it lives inside it. */
|
|
751
|
+
function imagePathForNote(pathValue, cwd) {
|
|
752
|
+
const file = String(pathValue ?? '').replace(/\\/gu, '/');
|
|
753
|
+
const root = normalizedCwd(cwd);
|
|
754
|
+
if (root === '') return file;
|
|
755
|
+
// Windows drive / UNC spellings are case-insensitive; POSIX ones are not.
|
|
756
|
+
const windows = /^[A-Za-z]:\//u.test(root) || root.startsWith('//');
|
|
757
|
+
const compareFile = windows ? file.toLowerCase() : file;
|
|
758
|
+
const compareRoot = windows ? root.toLowerCase() : root;
|
|
759
|
+
if (compareFile === compareRoot) return '.';
|
|
760
|
+
if (compareFile.startsWith(`${compareRoot}/`)) return file.slice(root.length + 1);
|
|
761
|
+
return file;
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
function absoluteImagePath(pathValue, cwd) {
|
|
765
|
+
const file = String(pathValue ?? '').replace(/\\/gu, '/');
|
|
766
|
+
if (isAbsolutePath(file)) return file;
|
|
767
|
+
const root = normalizedCwd(cwd);
|
|
768
|
+
return root === '' ? file : `${root}/${file.replace(/^\/+/u, '')}`;
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
/** Authenticated host file route used by the browser image renderer. */
|
|
772
|
+
function imageUrl(pathValue, cwd) {
|
|
773
|
+
const target = absoluteImagePath(pathValue, cwd);
|
|
774
|
+
try {
|
|
775
|
+
const base = typeof document !== 'undefined' && typeof document.baseURI === 'string'
|
|
776
|
+
? document.baseURI
|
|
777
|
+
: 'http://localhost/';
|
|
778
|
+
const url = new URL('api/file', base);
|
|
779
|
+
url.searchParams.set('path', target);
|
|
780
|
+
return url.href;
|
|
781
|
+
} catch {
|
|
782
|
+
return `/api/file?path=${encodeURIComponent(target)}`;
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
function imageMimeOf(file) {
|
|
787
|
+
const type = typeof file?.type === 'string' ? file.type.toLowerCase() : '';
|
|
788
|
+
if (type !== '') return Object.prototype.hasOwnProperty.call(IMAGE_MIME, type) ? type : '';
|
|
789
|
+
const name = String(file?.name ?? '');
|
|
790
|
+
if (/\.png$/iu.test(name)) return 'image/png';
|
|
791
|
+
if (/\.jpe?g$/iu.test(name)) return 'image/jpeg';
|
|
792
|
+
if (/\.webp$/iu.test(name)) return 'image/webp';
|
|
793
|
+
if (/\.gif$/iu.test(name)) return 'image/gif';
|
|
794
|
+
return '';
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
function isSupportedImage(file) {
|
|
798
|
+
return imageMimeOf(file) !== '';
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
function hostPathOf(file) {
|
|
802
|
+
try {
|
|
803
|
+
const bridge = globalThis.__DSH_HOST_PATHS__;
|
|
804
|
+
if (bridge === null || bridge === undefined || typeof bridge.pathFor !== 'function') return '';
|
|
805
|
+
const value = bridge.pathFor(file);
|
|
806
|
+
return typeof value === 'string' ? value : '';
|
|
807
|
+
} catch {
|
|
808
|
+
return '';
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
class ImageError extends Error {
|
|
813
|
+
constructor(code, message) {
|
|
814
|
+
super(message ?? code);
|
|
815
|
+
this.name = 'ImageError';
|
|
816
|
+
this.code = code;
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
function readFileBase64(file) {
|
|
821
|
+
return new Promise((resolve, reject) => {
|
|
822
|
+
try {
|
|
823
|
+
const reader = new FileReader();
|
|
824
|
+
reader.onerror = () => reject(new ImageError('read-failed', 'Could not read the image file.'));
|
|
825
|
+
reader.onload = () => {
|
|
826
|
+
const result = typeof reader.result === 'string' ? reader.result : '';
|
|
827
|
+
const comma = result.indexOf(',');
|
|
828
|
+
resolve(comma >= 0 ? result.slice(comma + 1) : result);
|
|
829
|
+
};
|
|
830
|
+
reader.readAsDataURL(file);
|
|
831
|
+
} catch (error) {
|
|
832
|
+
reject(error);
|
|
833
|
+
}
|
|
834
|
+
});
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
async function uploadImage(root, sessionId, file) {
|
|
838
|
+
if (Number(file?.size) > MAX_IMAGE_BYTES) throw new ImageError('too-large');
|
|
839
|
+
const mime = imageMimeOf(file);
|
|
840
|
+
if (mime === '') throw new ImageError('unsupported');
|
|
841
|
+
const data = await readFileBase64(file);
|
|
842
|
+
const response = await fetch(`${API}/image`, {
|
|
843
|
+
method: 'POST',
|
|
844
|
+
headers: { 'Content-Type': 'application/json' },
|
|
845
|
+
body: JSON.stringify({
|
|
846
|
+
root,
|
|
847
|
+
sessionId,
|
|
848
|
+
name: typeof file?.name === 'string' && file.name !== '' ? file.name : 'image.png',
|
|
849
|
+
mime,
|
|
850
|
+
data,
|
|
851
|
+
}),
|
|
852
|
+
});
|
|
853
|
+
const payload = await readResponse(response);
|
|
854
|
+
const image = normalizeImage(payload);
|
|
855
|
+
if (image === null) throw new ImageError('upload-failed', 'The host did not return an image path.');
|
|
856
|
+
return image;
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
function autoGrow(element) {
|
|
860
|
+
if (element === null || element === undefined || element.style === undefined) return;
|
|
861
|
+
try {
|
|
862
|
+
element.style.height = 'auto';
|
|
863
|
+
const height = Number(element.scrollHeight);
|
|
864
|
+
if (Number.isFinite(height) && height > 0) {
|
|
865
|
+
element.style.height = `${Math.min(240, Math.max(26, height))}px`;
|
|
866
|
+
}
|
|
867
|
+
} catch {
|
|
868
|
+
/* A non-layout shim (tests) may not expose scrollHeight. */
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
|
|
265
872
|
function workspaceName(root) {
|
|
266
873
|
const parts = String(root ?? '').split(/[\\/]+/u).filter(Boolean);
|
|
267
874
|
return parts.length === 0 ? String(root ?? '') : parts[parts.length - 1];
|
|
268
875
|
}
|
|
269
876
|
|
|
270
877
|
function readStoredUi() {
|
|
271
|
-
const base = {
|
|
878
|
+
const base = {
|
|
879
|
+
x: null,
|
|
880
|
+
y: null,
|
|
881
|
+
width: DEFAULT_W,
|
|
882
|
+
height: DEFAULT_H,
|
|
883
|
+
minimized: false,
|
|
884
|
+
hidden: false,
|
|
885
|
+
showArchive: false,
|
|
886
|
+
};
|
|
272
887
|
try {
|
|
273
888
|
const raw = window.localStorage?.getItem(UI_KEY);
|
|
274
889
|
if (typeof raw !== 'string' || raw === '') return base;
|
|
@@ -277,8 +892,11 @@ window.__ModuleLoader__.load({
|
|
|
277
892
|
return {
|
|
278
893
|
x: Number.isFinite(parsed.x) ? parsed.x : null,
|
|
279
894
|
y: Number.isFinite(parsed.y) ? parsed.y : null,
|
|
895
|
+
width: Number.isFinite(parsed.width) ? parsed.width : DEFAULT_W,
|
|
896
|
+
height: Number.isFinite(parsed.height) ? parsed.height : DEFAULT_H,
|
|
280
897
|
minimized: parsed.minimized === true,
|
|
281
898
|
hidden: parsed.hidden === true,
|
|
899
|
+
showArchive: parsed.showArchive === true,
|
|
282
900
|
};
|
|
283
901
|
} catch {
|
|
284
902
|
return base;
|
|
@@ -288,15 +906,18 @@ window.__ModuleLoader__.load({
|
|
|
288
906
|
function clampUi(ui) {
|
|
289
907
|
const vw = (typeof window !== 'undefined' && window.innerWidth) || 1280;
|
|
290
908
|
const vh = (typeof window !== 'undefined' && window.innerHeight) || 800;
|
|
291
|
-
const
|
|
292
|
-
const
|
|
909
|
+
const widthLimit = Math.max(240, vw - 16);
|
|
910
|
+
const heightLimit = Math.max(180, vh - 16);
|
|
911
|
+
const width = Math.min(Math.max(MIN_W, Number(ui.width) || DEFAULT_W), widthLimit);
|
|
912
|
+
const height = Math.min(Math.max(MIN_H, Number(ui.height) || DEFAULT_H), heightLimit);
|
|
913
|
+
const displayHeight = ui.minimized ? HEAD_H : height;
|
|
293
914
|
const x = ui.x === null || ui.x === undefined
|
|
294
915
|
? vw - width - 24
|
|
295
916
|
: Math.min(Math.max(8, ui.x), Math.max(8, vw - width - 8));
|
|
296
917
|
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 };
|
|
918
|
+
? vh - displayHeight - 24
|
|
919
|
+
: Math.min(Math.max(8, ui.y), Math.max(8, vh - displayHeight - 8));
|
|
920
|
+
return { ...ui, x, y, width, height };
|
|
300
921
|
}
|
|
301
922
|
|
|
302
923
|
async function readResponse(response) {
|
|
@@ -321,11 +942,11 @@ window.__ModuleLoader__.load({
|
|
|
321
942
|
return readResponse(response);
|
|
322
943
|
}
|
|
323
944
|
|
|
324
|
-
async function saveNote(root, sessionId,
|
|
945
|
+
async function saveNote(root, sessionId, segments) {
|
|
325
946
|
const response = await fetch(`${API}/note`, {
|
|
326
947
|
method: 'PUT',
|
|
327
948
|
headers: { 'Content-Type': 'application/json' },
|
|
328
|
-
body: JSON.stringify({ root, sessionId,
|
|
949
|
+
body: JSON.stringify({ root, sessionId, segments }),
|
|
329
950
|
});
|
|
330
951
|
return readResponse(response);
|
|
331
952
|
}
|
|
@@ -390,17 +1011,22 @@ window.__ModuleLoader__.load({
|
|
|
390
1011
|
function NotepadOverlay({ ctx, t }) {
|
|
391
1012
|
const [binding, setBinding] = useState(null);
|
|
392
1013
|
const [present, setPresent] = useState(presentConversation);
|
|
393
|
-
const [
|
|
394
|
-
const [
|
|
1014
|
+
const [segments, setSegments] = useState([]);
|
|
1015
|
+
const [selected, setSelected] = useState(() => new Set());
|
|
395
1016
|
const [status, setStatus] = useState('idle');
|
|
396
1017
|
const [error, setError] = useState('');
|
|
397
1018
|
const [savedAt, setSavedAt] = useState(null);
|
|
398
1019
|
const [toast, setToast] = useState(null);
|
|
399
1020
|
const [ui, setUi] = useState(readStoredUi);
|
|
1021
|
+
const [sending, setSending] = useState(false);
|
|
1022
|
+
const [resizing, setResizing] = useState(false);
|
|
1023
|
+
const [uploading, setUploading] = useState({});
|
|
1024
|
+
const [dragSegment, setDragSegment] = useState('');
|
|
400
1025
|
|
|
401
1026
|
const bindingRef = useRef(null);
|
|
402
1027
|
const rootSessionRef = useRef('');
|
|
403
|
-
const
|
|
1028
|
+
const segmentsRef = useRef([]);
|
|
1029
|
+
const selectedRef = useRef(selected);
|
|
404
1030
|
const rootRef = useRef('');
|
|
405
1031
|
const versionRef = useRef(null);
|
|
406
1032
|
const dirtyRef = useRef(false);
|
|
@@ -408,9 +1034,35 @@ window.__ModuleLoader__.load({
|
|
|
408
1034
|
const toastTimerRef = useRef(0);
|
|
409
1035
|
const seqRef = useRef(0);
|
|
410
1036
|
const saveNowRef = useRef(null);
|
|
1037
|
+
const reloadRef = useRef(null);
|
|
1038
|
+
const loadFailedRef = useRef(false);
|
|
1039
|
+
const loadSeqRef = useRef(0);
|
|
411
1040
|
const inputActionsRef = useRef(null);
|
|
412
|
-
const taRef = useRef(null);
|
|
413
1041
|
const dragRef = useRef(null);
|
|
1042
|
+
const resizeRef = useRef(null);
|
|
1043
|
+
const sendingRef = useRef(false);
|
|
1044
|
+
const textareaRefs = useRef(new Map());
|
|
1045
|
+
const fileInputRefs = useRef(new Map());
|
|
1046
|
+
const listRef = useRef(null);
|
|
1047
|
+
const pendingFocusRef = useRef(null);
|
|
1048
|
+
const undoRef = useRef(null);
|
|
1049
|
+
|
|
1050
|
+
const setSelection = (next) => {
|
|
1051
|
+
selectedRef.current = next;
|
|
1052
|
+
setSelected(next);
|
|
1053
|
+
};
|
|
1054
|
+
|
|
1055
|
+
const pruneSelectionFor = (nextSegments) => {
|
|
1056
|
+
if (selectedRef.current.size === 0) return;
|
|
1057
|
+
const activeIds = new Set(nextSegments.filter((segment) => segment.archived !== true).map((segment) => segment.id));
|
|
1058
|
+
const pruned = new Set();
|
|
1059
|
+
let changed = false;
|
|
1060
|
+
for (const id of selectedRef.current) {
|
|
1061
|
+
if (activeIds.has(id)) pruned.add(id);
|
|
1062
|
+
else changed = true;
|
|
1063
|
+
}
|
|
1064
|
+
if (changed) setSelection(pruned);
|
|
1065
|
+
};
|
|
414
1066
|
|
|
415
1067
|
// Session + workspace -------------------------------------------------
|
|
416
1068
|
useEffect(() => {
|
|
@@ -458,7 +1110,7 @@ window.__ModuleLoader__.load({
|
|
|
458
1110
|
return () => clearInterval(timer);
|
|
459
1111
|
}, []);
|
|
460
1112
|
|
|
461
|
-
// Position
|
|
1113
|
+
// Position + size ----------------------------------------------------
|
|
462
1114
|
useEffect(() => {
|
|
463
1115
|
setUi((previous) => clampUi(previous));
|
|
464
1116
|
const onResize = () => setUi((previous) => clampUi(previous));
|
|
@@ -482,10 +1134,10 @@ window.__ModuleLoader__.load({
|
|
|
482
1134
|
}
|
|
483
1135
|
};
|
|
484
1136
|
|
|
485
|
-
const showToast = (message) => {
|
|
486
|
-
setToast({ id: Date.now(), message });
|
|
1137
|
+
const showToast = (message, action) => {
|
|
1138
|
+
setToast({ id: Date.now(), message, action: action ?? null });
|
|
487
1139
|
if (toastTimerRef.current) clearTimeout(toastTimerRef.current);
|
|
488
|
-
toastTimerRef.current = setTimeout(() => setToast(null), 1800);
|
|
1140
|
+
toastTimerRef.current = setTimeout(() => setToast(null), action === undefined || action === null ? 1800 : 5000);
|
|
489
1141
|
};
|
|
490
1142
|
|
|
491
1143
|
const scheduleSave = () => {
|
|
@@ -502,7 +1154,8 @@ window.__ModuleLoader__.load({
|
|
|
502
1154
|
const sessionId = rootSessionRef.current;
|
|
503
1155
|
if (root === '' || sessionId === '') return;
|
|
504
1156
|
if (!dirtyRef.current && options.force !== true) return;
|
|
505
|
-
const body =
|
|
1157
|
+
const body = serializeSegments(segmentsRef.current);
|
|
1158
|
+
const signature = JSON.stringify(body);
|
|
506
1159
|
const seq = ++seqRef.current;
|
|
507
1160
|
dirtyRef.current = false;
|
|
508
1161
|
setStatus('saving');
|
|
@@ -512,7 +1165,7 @@ window.__ModuleLoader__.load({
|
|
|
512
1165
|
if (seq !== seqRef.current || rootRef.current !== root) return;
|
|
513
1166
|
if (typeof data.mtimeMs === 'number') versionRef.current = data.mtimeMs;
|
|
514
1167
|
setSavedAt(typeof data.mtimeMs === 'number' ? data.mtimeMs : Date.now());
|
|
515
|
-
if (
|
|
1168
|
+
if (JSON.stringify(serializeSegments(segmentsRef.current)) === signature) {
|
|
516
1169
|
dirtyRef.current = false;
|
|
517
1170
|
setStatus('saved');
|
|
518
1171
|
} else {
|
|
@@ -529,12 +1182,25 @@ window.__ModuleLoader__.load({
|
|
|
529
1182
|
};
|
|
530
1183
|
saveNowRef.current = saveNow;
|
|
531
1184
|
|
|
1185
|
+
const commitSegments = (next, options = {}) => {
|
|
1186
|
+
segmentsRef.current = next;
|
|
1187
|
+
setSegments(next);
|
|
1188
|
+
pruneSelectionFor(next);
|
|
1189
|
+
if (options.focus !== undefined) pendingFocusRef.current = options.focus;
|
|
1190
|
+
dirtyRef.current = true;
|
|
1191
|
+
setStatus('dirty');
|
|
1192
|
+
setError('');
|
|
1193
|
+
scheduleSave();
|
|
1194
|
+
};
|
|
1195
|
+
|
|
532
1196
|
// Load on workspace/session change ------------------------------------
|
|
533
1197
|
useEffect(() => {
|
|
534
1198
|
if (binding === null) {
|
|
535
1199
|
rootRef.current = '';
|
|
536
1200
|
rootSessionRef.current = '';
|
|
537
|
-
|
|
1201
|
+
segmentsRef.current = [];
|
|
1202
|
+
setSegments([]);
|
|
1203
|
+
setSelection(new Set());
|
|
538
1204
|
setStatus('idle');
|
|
539
1205
|
setError('');
|
|
540
1206
|
return undefined;
|
|
@@ -547,34 +1213,76 @@ window.__ModuleLoader__.load({
|
|
|
547
1213
|
rootRef.current = root;
|
|
548
1214
|
rootSessionRef.current = binding.id;
|
|
549
1215
|
let cancelled = false;
|
|
550
|
-
|
|
1216
|
+
loadFailedRef.current = false;
|
|
1217
|
+
segmentsRef.current = [];
|
|
1218
|
+
setSegments([]);
|
|
1219
|
+
setSelection(new Set());
|
|
551
1220
|
setStatus('loading');
|
|
552
1221
|
setError('');
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
1222
|
+
const load = () => {
|
|
1223
|
+
const mySeq = ++loadSeqRef.current;
|
|
1224
|
+
loadFailedRef.current = false;
|
|
1225
|
+
setStatus('loading');
|
|
1226
|
+
setError('');
|
|
1227
|
+
fetchNote(root, binding.id)
|
|
1228
|
+
.then((data) => {
|
|
1229
|
+
if (cancelled || rootRef.current !== root || mySeq !== loadSeqRef.current) return;
|
|
1230
|
+
let value = normalizeServerSegments(data);
|
|
1231
|
+
if (value.length === 0) value = [newSegment('')];
|
|
1232
|
+
segmentsRef.current = value;
|
|
1233
|
+
setSegments(value);
|
|
1234
|
+
setSelection(new Set());
|
|
1235
|
+
versionRef.current = typeof data.mtimeMs === 'number' ? data.mtimeMs : null;
|
|
1236
|
+
setSavedAt(typeof data.mtimeMs === 'number' ? data.mtimeMs : null);
|
|
1237
|
+
dirtyRef.current = false;
|
|
1238
|
+
setStatus('saved');
|
|
1239
|
+
})
|
|
1240
|
+
.catch((reason) => {
|
|
1241
|
+
if (cancelled || rootRef.current !== root || mySeq !== loadSeqRef.current) return;
|
|
1242
|
+
loadFailedRef.current = true;
|
|
1243
|
+
setStatus('error');
|
|
1244
|
+
setError(String(reason?.message || reason));
|
|
1245
|
+
});
|
|
1246
|
+
};
|
|
1247
|
+
reloadRef.current = load;
|
|
1248
|
+
load();
|
|
573
1249
|
return () => {
|
|
574
1250
|
cancelled = true;
|
|
1251
|
+
if (reloadRef.current === load) reloadRef.current = null;
|
|
575
1252
|
};
|
|
576
1253
|
}, [binding === null ? '' : binding.id, binding === null ? '' : binding.cwd]);
|
|
577
1254
|
|
|
1255
|
+
// Focus a just-created or restored card ------------------------------
|
|
1256
|
+
useEffect(() => {
|
|
1257
|
+
const pending = pendingFocusRef.current;
|
|
1258
|
+
if (pending === null || pending === undefined) return;
|
|
1259
|
+
pendingFocusRef.current = null;
|
|
1260
|
+
const element = textareaRefs.current.get(pending.id);
|
|
1261
|
+
if (element === undefined || element === null) return;
|
|
1262
|
+
try {
|
|
1263
|
+
element.focus?.({ preventScroll: true });
|
|
1264
|
+
} catch {
|
|
1265
|
+
try {
|
|
1266
|
+
element.focus?.();
|
|
1267
|
+
} catch {
|
|
1268
|
+
/* ignore */
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
try {
|
|
1272
|
+
const value = typeof element.value === 'string' ? element.value : '';
|
|
1273
|
+
const caret = Math.max(0, Math.min(Number(pending.caret) || 0, value.length));
|
|
1274
|
+
if (typeof element.setSelectionRange === 'function') element.setSelectionRange(caret, caret);
|
|
1275
|
+
} catch {
|
|
1276
|
+
/* ignore */
|
|
1277
|
+
}
|
|
1278
|
+
try {
|
|
1279
|
+
element.scrollIntoView?.({ block: 'nearest' });
|
|
1280
|
+
} catch {
|
|
1281
|
+
/* ignore */
|
|
1282
|
+
}
|
|
1283
|
+
autoGrow(element);
|
|
1284
|
+
}, [segments]);
|
|
1285
|
+
|
|
578
1286
|
// Keyboard ------------------------------------------------------------
|
|
579
1287
|
useEffect(() => {
|
|
580
1288
|
const onKeyDown = (event) => {
|
|
@@ -620,14 +1328,18 @@ window.__ModuleLoader__.load({
|
|
|
620
1328
|
const root = rootRef.current;
|
|
621
1329
|
const sessionId = rootSessionRef.current;
|
|
622
1330
|
if (root === '' || sessionId === '' || dirtyRef.current || timerRef.current) return;
|
|
623
|
-
|
|
1331
|
+
const active = typeof document !== 'undefined' ? document.activeElement : null;
|
|
1332
|
+
if (active !== null && active !== undefined && typeof active.closest === 'function'
|
|
1333
|
+
&& active.closest('[data-dsh-notepad="true"]') !== null) return;
|
|
624
1334
|
fetchNote(root, sessionId)
|
|
625
1335
|
.then((data) => {
|
|
626
1336
|
if (rootRef.current !== root || dirtyRef.current) return;
|
|
627
1337
|
if (typeof data.mtimeMs === 'number' && data.mtimeMs === versionRef.current) return;
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
1338
|
+
let value = normalizeServerSegments(data);
|
|
1339
|
+
if (value.length === 0) value = [newSegment('')];
|
|
1340
|
+
segmentsRef.current = value;
|
|
1341
|
+
setSegments(value);
|
|
1342
|
+
setSelection(new Set());
|
|
631
1343
|
versionRef.current = typeof data.mtimeMs === 'number' ? data.mtimeMs : null;
|
|
632
1344
|
setSavedAt(typeof data.mtimeMs === 'number' ? data.mtimeMs : null);
|
|
633
1345
|
setStatus('saved');
|
|
@@ -649,55 +1361,433 @@ window.__ModuleLoader__.load({
|
|
|
649
1361
|
};
|
|
650
1362
|
}, []);
|
|
651
1363
|
|
|
652
|
-
//
|
|
653
|
-
const
|
|
654
|
-
const
|
|
655
|
-
|
|
656
|
-
|
|
1364
|
+
// Segment mutations ---------------------------------------------------
|
|
1365
|
+
const updateSegmentText = (id, value) => {
|
|
1366
|
+
const next = segmentsRef.current.map((segment) => (segment.id === id ? { ...segment, text: value } : segment));
|
|
1367
|
+
segmentsRef.current = next;
|
|
1368
|
+
setSegments(next);
|
|
657
1369
|
dirtyRef.current = true;
|
|
658
1370
|
setStatus('dirty');
|
|
659
1371
|
setError('');
|
|
660
1372
|
scheduleSave();
|
|
661
1373
|
};
|
|
662
1374
|
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
1375
|
+
// Image attachments ---------------------------------------------------
|
|
1376
|
+
const imageErrorText = (reason) => {
|
|
1377
|
+
const code = reason?.code;
|
|
1378
|
+
if (code === 'too-large') return t('imageTooLarge');
|
|
1379
|
+
if (code === 'unsupported') return t('imageUnsupported');
|
|
1380
|
+
if (code === 'too-many') return t('imageTooMany', { n: MAX_IMAGES });
|
|
1381
|
+
return `${t('imageFailed')}: ${String(reason?.message ?? reason)}`;
|
|
1382
|
+
};
|
|
1383
|
+
|
|
1384
|
+
const appendImages = (id, images) => {
|
|
1385
|
+
if (!Array.isArray(images) || images.length === 0) return 0;
|
|
1386
|
+
const target = segmentsRef.current.find((segment) => segment.id === id);
|
|
1387
|
+
if (target === undefined || target.archived === true) return 0;
|
|
1388
|
+
const existing = Array.isArray(target.images) ? target.images : [];
|
|
1389
|
+
const room = Math.max(0, MAX_IMAGES - existing.length);
|
|
1390
|
+
if (room === 0) {
|
|
1391
|
+
showToast(t('imageTooMany', { n: MAX_IMAGES }));
|
|
1392
|
+
return 0;
|
|
1393
|
+
}
|
|
1394
|
+
const accepted = images.slice(0, room);
|
|
1395
|
+
const next = segmentsRef.current.map((segment) =>
|
|
1396
|
+
(segment.id === id ? { ...segment, images: [...existing, ...accepted] } : segment));
|
|
1397
|
+
commitSegments(next);
|
|
1398
|
+
if (accepted.length < images.length) showToast(t('imageTooMany', { n: MAX_IMAGES }));
|
|
1399
|
+
else showToast(t('imageAdded', { n: accepted.length }));
|
|
1400
|
+
return accepted.length;
|
|
1401
|
+
};
|
|
1402
|
+
|
|
1403
|
+
/**
|
|
1404
|
+
* Add browser files to one segment. Pasted images are always written into
|
|
1405
|
+
* the plugin directory; selected images use their host path when the
|
|
1406
|
+
* desktop bridge exposes it and fall back to the same upload otherwise.
|
|
1407
|
+
* @param {string} id segment id
|
|
1408
|
+
* @param {ArrayLike<File>} files
|
|
1409
|
+
* @param {'paste'|'pick'} origin
|
|
1410
|
+
*/
|
|
1411
|
+
const attachImages = async (id, files, origin) => {
|
|
1412
|
+
const list = Array.from(files ?? []).filter(isSupportedImage);
|
|
1413
|
+
if (list.length === 0) {
|
|
1414
|
+
showToast(t('imageUnsupported'));
|
|
667
1415
|
return;
|
|
668
1416
|
}
|
|
669
|
-
const
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
1417
|
+
const root = rootRef.current;
|
|
1418
|
+
const sessionId = rootSessionRef.current;
|
|
1419
|
+
if (root === '' || sessionId === '') return;
|
|
1420
|
+
setUploading((previous) => ({ ...previous, [id]: (previous[id] ?? 0) + list.length }));
|
|
1421
|
+
const collected = [];
|
|
1422
|
+
try {
|
|
1423
|
+
for (const file of list) {
|
|
1424
|
+
const directPath = origin === 'pick' ? hostPathOf(file) : '';
|
|
1425
|
+
if (directPath !== '') {
|
|
1426
|
+
collected.push({
|
|
1427
|
+
path: imagePathForNote(directPath, bindingRef.current?.cwd ?? root),
|
|
1428
|
+
name: baseNameOf(directPath) || file?.name || 'image',
|
|
1429
|
+
});
|
|
1430
|
+
continue;
|
|
682
1431
|
}
|
|
683
|
-
|
|
684
|
-
/* fall through to copy */
|
|
1432
|
+
collected.push(await uploadImage(root, sessionId, file));
|
|
685
1433
|
}
|
|
1434
|
+
if (collected.length > 0) appendImages(id, collected);
|
|
1435
|
+
} catch (reason) {
|
|
1436
|
+
if (collected.length > 0) appendImages(id, collected);
|
|
1437
|
+
showToast(imageErrorText(reason));
|
|
1438
|
+
} finally {
|
|
1439
|
+
setUploading((previous) => {
|
|
1440
|
+
const next = { ...previous };
|
|
1441
|
+
const remaining = (next[id] ?? 0) - list.length;
|
|
1442
|
+
if (remaining > 0) next[id] = remaining;
|
|
1443
|
+
else delete next[id];
|
|
1444
|
+
return next;
|
|
1445
|
+
});
|
|
686
1446
|
}
|
|
687
|
-
const copied = await copyText(value);
|
|
688
|
-
showToast(copied ? t('insertUnavailable') : t('copyFailed'));
|
|
689
1447
|
};
|
|
690
1448
|
|
|
691
|
-
const
|
|
692
|
-
const
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
return;
|
|
696
|
-
}
|
|
1449
|
+
const removeImage = (id, pathValue) => {
|
|
1450
|
+
const next = segmentsRef.current.map((segment) => {
|
|
1451
|
+
if (segment.id !== id) return segment;
|
|
1452
|
+
const images = Array.isArray(segment.images) ? segment.images.filter((image) => image.path !== pathValue) : [];
|
|
1453
|
+
return { ...segment, images };
|
|
1454
|
+
});
|
|
1455
|
+
commitSegments(next);
|
|
1456
|
+
};
|
|
1457
|
+
|
|
1458
|
+
const triggerFilePicker = (id) => {
|
|
1459
|
+
const input = fileInputRefs.current.get(id);
|
|
1460
|
+
if (input !== undefined && input !== null && typeof input.click === 'function') input.click();
|
|
1461
|
+
};
|
|
1462
|
+
|
|
1463
|
+
const onFileInputChange = (event, id) => {
|
|
1464
|
+
const input = event?.currentTarget ?? event?.target;
|
|
1465
|
+
const files = input?.files;
|
|
1466
|
+
if (files !== undefined && files !== null && files.length > 0) attachImages(id, files, 'pick');
|
|
1467
|
+
try {
|
|
1468
|
+
if (input !== null && input !== undefined) input.value = '';
|
|
1469
|
+
} catch {
|
|
1470
|
+
/* A read-only file input value must not break the flow. */
|
|
1471
|
+
}
|
|
1472
|
+
};
|
|
1473
|
+
|
|
1474
|
+
const clipboardImageFiles = (data) => {
|
|
1475
|
+
const files = data?.files;
|
|
1476
|
+
if (files === undefined || files === null) return [];
|
|
1477
|
+
return Array.from(files).filter(isSupportedImage);
|
|
1478
|
+
};
|
|
1479
|
+
|
|
1480
|
+
const onSegmentPaste = (event, id) => {
|
|
1481
|
+
const data = event?.clipboardData ?? event?.nativeEvent?.clipboardData;
|
|
1482
|
+
const files = clipboardImageFiles(data);
|
|
1483
|
+
if (files.length === 0) return;
|
|
1484
|
+
event.preventDefault?.();
|
|
1485
|
+
event.stopPropagation?.();
|
|
1486
|
+
attachImages(id, files, 'paste');
|
|
1487
|
+
};
|
|
1488
|
+
|
|
1489
|
+
const onSegmentDrop = (event, id) => {
|
|
1490
|
+
const files = clipboardImageFiles(event?.dataTransfer);
|
|
1491
|
+
setDragSegment('');
|
|
1492
|
+
if (files.length === 0) return;
|
|
1493
|
+
event.preventDefault?.();
|
|
1494
|
+
event.stopPropagation?.();
|
|
1495
|
+
attachImages(id, files, 'pick');
|
|
1496
|
+
};
|
|
1497
|
+
|
|
1498
|
+
const onSegmentDragOver = (event, id) => {
|
|
1499
|
+
const types = event?.dataTransfer?.types;
|
|
1500
|
+
if (types !== undefined && types !== null && !Array.from(types).includes('Files')) return;
|
|
1501
|
+
event.preventDefault?.();
|
|
1502
|
+
if (dragSegment !== id) setDragSegment(id);
|
|
1503
|
+
};
|
|
1504
|
+
|
|
1505
|
+
const onSegmentDragLeave = (event, id) => {
|
|
1506
|
+
const related = event?.relatedTarget;
|
|
1507
|
+
const current = event?.currentTarget;
|
|
1508
|
+
if (related !== null && related !== undefined && current !== null && current !== undefined
|
|
1509
|
+
&& typeof current.contains === 'function' && current.contains(related)) return;
|
|
1510
|
+
if (dragSegment === id) setDragSegment('');
|
|
1511
|
+
};
|
|
1512
|
+
|
|
1513
|
+
const openImage = (image) => {
|
|
1514
|
+
const url = imageUrl(image.path, rootRef.current);
|
|
1515
|
+
try {
|
|
1516
|
+
window.open(url, '_blank', 'noopener,noreferrer');
|
|
1517
|
+
} catch {
|
|
1518
|
+
/* Popups may be blocked; the path remains available in the title. */
|
|
1519
|
+
}
|
|
1520
|
+
};
|
|
1521
|
+
|
|
1522
|
+
const addSegment = () => {
|
|
1523
|
+
if (segmentsRef.current.length >= MAX_SEGMENTS) {
|
|
1524
|
+
showToast(t('tooManySegments', { n: MAX_SEGMENTS }));
|
|
1525
|
+
return;
|
|
1526
|
+
}
|
|
1527
|
+
const segment = newSegment('');
|
|
1528
|
+
commitSegments([...segmentsRef.current, segment], { focus: { id: segment.id, caret: 0 } });
|
|
1529
|
+
};
|
|
1530
|
+
|
|
1531
|
+
const splitSegment = (id, element) => {
|
|
1532
|
+
const current = segmentsRef.current.find((segment) => segment.id === id);
|
|
1533
|
+
if (current === undefined) return;
|
|
1534
|
+
const start = typeof element?.selectionStart === 'number' ? element.selectionStart : current.text.length;
|
|
1535
|
+
const head = current.text.slice(0, start);
|
|
1536
|
+
const tail = current.text.slice(start);
|
|
1537
|
+
const nextSegment = newSegment(tail);
|
|
1538
|
+
const next = [];
|
|
1539
|
+
for (const segment of segmentsRef.current) {
|
|
1540
|
+
if (segment.id === id) {
|
|
1541
|
+
next.push({ ...segment, text: head });
|
|
1542
|
+
next.push(nextSegment);
|
|
1543
|
+
} else {
|
|
1544
|
+
next.push(segment);
|
|
1545
|
+
}
|
|
1546
|
+
}
|
|
1547
|
+
commitSegments(next, { focus: { id: nextSegment.id, caret: 0 } });
|
|
1548
|
+
};
|
|
1549
|
+
|
|
1550
|
+
const archiveSegment = (id) => {
|
|
1551
|
+
const current = segmentsRef.current.find((segment) => segment.id === id && segment.archived !== true);
|
|
1552
|
+
if (current === undefined) return;
|
|
1553
|
+
const archivedAt = new Date().toISOString();
|
|
1554
|
+
const next = segmentsRef.current.map((segment) =>
|
|
1555
|
+
(segment.id === id ? { ...segment, archived: true, archivedAt } : segment));
|
|
1556
|
+
setSelection(new Set([...selectedRef.current].filter((value) => value !== id)));
|
|
1557
|
+
commitSegments(next);
|
|
1558
|
+
showToast(t('archivedToast'));
|
|
1559
|
+
};
|
|
1560
|
+
|
|
1561
|
+
const archiveSelected = () => {
|
|
1562
|
+
const ids = new Set(
|
|
1563
|
+
segmentsRef.current
|
|
1564
|
+
.filter((segment) => segment.archived !== true && selectedRef.current.has(segment.id))
|
|
1565
|
+
.map((segment) => segment.id),
|
|
1566
|
+
);
|
|
1567
|
+
if (ids.size === 0) return;
|
|
1568
|
+
const archivedAt = new Date().toISOString();
|
|
1569
|
+
const next = segmentsRef.current.map((segment) =>
|
|
1570
|
+
(ids.has(segment.id) ? { ...segment, archived: true, archivedAt } : segment));
|
|
1571
|
+
setSelection(new Set());
|
|
1572
|
+
commitSegments(next);
|
|
1573
|
+
showToast(t('archivedCount', { n: ids.size }));
|
|
1574
|
+
};
|
|
1575
|
+
|
|
1576
|
+
const restoreSegment = (id) => {
|
|
1577
|
+
const current = segmentsRef.current.find((segment) => segment.id === id && segment.archived === true);
|
|
1578
|
+
if (current === undefined) return;
|
|
1579
|
+
const next = segmentsRef.current.map((segment) =>
|
|
1580
|
+
(segment.id === id ? { ...segment, archived: false, archivedAt: null } : segment));
|
|
1581
|
+
commitSegments(next, { focus: { id, caret: current.text.length } });
|
|
1582
|
+
showToast(t('restoredToast'));
|
|
1583
|
+
};
|
|
1584
|
+
|
|
1585
|
+
const restoreAll = () => {
|
|
1586
|
+
const restored = segmentsRef.current.filter((segment) => segment.archived === true);
|
|
1587
|
+
if (restored.length === 0) return;
|
|
1588
|
+
const next = segmentsRef.current.map((segment) =>
|
|
1589
|
+
(segment.archived === true ? { ...segment, archived: false, archivedAt: null } : segment));
|
|
1590
|
+
commitSegments(next);
|
|
1591
|
+
showToast(t('restoredCount', { n: restored.length }));
|
|
1592
|
+
};
|
|
1593
|
+
|
|
1594
|
+
const undoLast = () => {
|
|
1595
|
+
const record = undoRef.current;
|
|
1596
|
+
if (record === null || record === undefined) return;
|
|
1597
|
+
undoRef.current = null;
|
|
1598
|
+
commitSegments(record.snapshot);
|
|
1599
|
+
setToast(null);
|
|
1600
|
+
};
|
|
1601
|
+
|
|
1602
|
+
const deleteArchived = (id) => {
|
|
1603
|
+
const target = segmentsRef.current.find((segment) => segment.id === id && segment.archived === true);
|
|
1604
|
+
if (target === undefined) return;
|
|
1605
|
+
undoRef.current = { snapshot: segmentsRef.current };
|
|
1606
|
+
const next = segmentsRef.current.filter((segment) => segment.id !== id);
|
|
1607
|
+
commitSegments(next);
|
|
1608
|
+
showToast(t('deletedToast'), { label: t('undo'), onClick: undoLast });
|
|
1609
|
+
};
|
|
1610
|
+
|
|
1611
|
+
const clearArchive = () => {
|
|
1612
|
+
const archived = segmentsRef.current.filter((segment) => segment.archived === true);
|
|
1613
|
+
if (archived.length === 0) return;
|
|
1614
|
+
undoRef.current = { snapshot: segmentsRef.current };
|
|
1615
|
+
const next = segmentsRef.current.filter((segment) => segment.archived !== true);
|
|
1616
|
+
commitSegments(next);
|
|
1617
|
+
showToast(t('clearedArchive', { n: archived.length }), { label: t('undo'), onClick: undoLast });
|
|
1618
|
+
};
|
|
1619
|
+
|
|
1620
|
+
// Selection -----------------------------------------------------------
|
|
1621
|
+
const toggleSelected = (id) => {
|
|
1622
|
+
const next = new Set(selectedRef.current);
|
|
1623
|
+
if (next.has(id)) next.delete(id);
|
|
1624
|
+
else next.add(id);
|
|
1625
|
+
setSelection(next);
|
|
1626
|
+
};
|
|
1627
|
+
|
|
1628
|
+
const toggleSelectAll = () => {
|
|
1629
|
+
const active = segmentsRef.current.filter((segment) => segment.archived !== true);
|
|
1630
|
+
const allSelected = active.length > 0 && active.every((segment) => selectedRef.current.has(segment.id));
|
|
1631
|
+
setSelection(allSelected ? new Set() : new Set(active.map((segment) => segment.id)));
|
|
1632
|
+
};
|
|
1633
|
+
|
|
1634
|
+
// Composer actions ----------------------------------------------------
|
|
1635
|
+
const insertValue = (value) => {
|
|
1636
|
+
const actions = inputActionsRef.current;
|
|
1637
|
+
if (actions === null || actions === undefined) return false;
|
|
1638
|
+
if (typeof actions.captureInsertion !== 'function' || typeof actions.insertText !== 'function') return false;
|
|
1639
|
+
try {
|
|
1640
|
+
const span = actions.captureInsertion();
|
|
1641
|
+
if (actions.insertText(value, span) === true) {
|
|
1642
|
+
actions.persistDraft?.();
|
|
1643
|
+
return true;
|
|
1644
|
+
}
|
|
1645
|
+
} catch {
|
|
1646
|
+
/* fall through to copy */
|
|
1647
|
+
}
|
|
1648
|
+
return false;
|
|
1649
|
+
};
|
|
1650
|
+
|
|
1651
|
+
const replaceValue = (value) => {
|
|
1652
|
+
const actions = inputActionsRef.current;
|
|
1653
|
+
if (actions === null || actions === undefined || typeof actions.setDraft !== 'function') return false;
|
|
1654
|
+
try {
|
|
1655
|
+
actions.setDraft(value);
|
|
1656
|
+
actions.persistDraft?.();
|
|
1657
|
+
return true;
|
|
1658
|
+
} catch {
|
|
1659
|
+
return false;
|
|
1660
|
+
}
|
|
1661
|
+
};
|
|
1662
|
+
|
|
1663
|
+
const insertSegments = async (list) => {
|
|
1664
|
+
const value = mergedText(list);
|
|
1665
|
+
if (value === '') {
|
|
1666
|
+
showToast(t('insertEmpty'));
|
|
1667
|
+
return;
|
|
1668
|
+
}
|
|
1669
|
+
if (insertValue(value)) {
|
|
1670
|
+
showToast(t('inserted'));
|
|
1671
|
+
return;
|
|
1672
|
+
}
|
|
1673
|
+
const copied = await copyText(value);
|
|
1674
|
+
showToast(copied ? t('insertUnavailable') : t('copyFailed'));
|
|
1675
|
+
};
|
|
1676
|
+
|
|
1677
|
+
const replaceSegments = async (list) => {
|
|
1678
|
+
const value = mergedText(list);
|
|
1679
|
+
if (value === '') {
|
|
1680
|
+
showToast(t('insertEmpty'));
|
|
1681
|
+
return;
|
|
1682
|
+
}
|
|
1683
|
+
if (replaceValue(value)) {
|
|
1684
|
+
showToast(t('replaced'));
|
|
1685
|
+
return;
|
|
1686
|
+
}
|
|
1687
|
+
const copied = await copyText(value);
|
|
1688
|
+
showToast(copied ? t('replaceUnavailable') : t('copyFailed'));
|
|
1689
|
+
};
|
|
1690
|
+
|
|
1691
|
+
/**
|
|
1692
|
+
* Send text into the current session. Prefers the documented scoped
|
|
1693
|
+
* conversation service; falls back to the session face with a local echo.
|
|
1694
|
+
* @returns {Promise<boolean>} whether a prompt was accepted
|
|
1695
|
+
*/
|
|
1696
|
+
const sendToSession = async (value) => {
|
|
1697
|
+
const sessionId = rootSessionRef.current !== '' ? rootSessionRef.current : bindingRef.current?.id;
|
|
1698
|
+
if (typeof sessionId !== 'string' || sessionId === '') return false;
|
|
1699
|
+
const sessions = ctx.get('sessions');
|
|
1700
|
+
if (sessions === undefined || sessions === null) return false;
|
|
1701
|
+
if (typeof sessions.scope === 'function') {
|
|
1702
|
+
const scoped = sessions.scope(sessionId);
|
|
1703
|
+
const conversation = scoped === undefined || scoped === null
|
|
1704
|
+
? undefined
|
|
1705
|
+
: typeof scoped.get === 'function' ? scoped.get('conversation') : scoped.conversation;
|
|
1706
|
+
if (conversation !== undefined && conversation !== null && typeof conversation.send === 'function') {
|
|
1707
|
+
await conversation.send(value);
|
|
1708
|
+
return true;
|
|
1709
|
+
}
|
|
1710
|
+
}
|
|
1711
|
+
if (typeof sessions.binding === 'function') {
|
|
1712
|
+
const liveBinding = sessions.binding(sessionId);
|
|
1713
|
+
const session = liveBinding?.session;
|
|
1714
|
+
if (session !== undefined && session !== null && typeof session.prompt === 'function') {
|
|
1715
|
+
let echo;
|
|
1716
|
+
try {
|
|
1717
|
+
echo = typeof session.beginSubmission === 'function'
|
|
1718
|
+
? session.beginSubmission({ mode: 'queue', text: value, attachments: [] })
|
|
1719
|
+
: undefined;
|
|
1720
|
+
} catch {
|
|
1721
|
+
echo = undefined;
|
|
1722
|
+
}
|
|
1723
|
+
try {
|
|
1724
|
+
const result = await session.prompt([{ type: 'text', text: value }], 'queue', undefined, echo?.requestId);
|
|
1725
|
+
if (result !== undefined && result !== null && result.ok === false) {
|
|
1726
|
+
throw new Error(result.error?.message ?? 'prompt failed');
|
|
1727
|
+
}
|
|
1728
|
+
return true;
|
|
1729
|
+
} catch (reason) {
|
|
1730
|
+
try {
|
|
1731
|
+
echo?.abandon?.();
|
|
1732
|
+
} catch {
|
|
1733
|
+
/* ignore */
|
|
1734
|
+
}
|
|
1735
|
+
throw reason;
|
|
1736
|
+
}
|
|
1737
|
+
}
|
|
1738
|
+
}
|
|
1739
|
+
return false;
|
|
1740
|
+
};
|
|
1741
|
+
|
|
1742
|
+
const sendSegments = async (list) => {
|
|
1743
|
+
if (sendingRef.current) return;
|
|
1744
|
+
const value = mergedText(list);
|
|
1745
|
+
if (value === '') {
|
|
1746
|
+
showToast(t('insertEmpty'));
|
|
1747
|
+
return;
|
|
1748
|
+
}
|
|
1749
|
+
sendingRef.current = true;
|
|
1750
|
+
setSending(true);
|
|
1751
|
+
const targetRoot = rootRef.current;
|
|
1752
|
+
const targetSession = rootSessionRef.current;
|
|
1753
|
+
try {
|
|
1754
|
+
const accepted = await sendToSession(value);
|
|
1755
|
+
if (accepted !== true) {
|
|
1756
|
+
if (!insertValue(value)) await copyText(value);
|
|
1757
|
+
showToast(t('sendUnavailable'));
|
|
1758
|
+
return;
|
|
1759
|
+
}
|
|
1760
|
+
showToast(t('sent'));
|
|
1761
|
+
const ids = new Set(
|
|
1762
|
+
list.filter((segment) => segment.archived !== true).map((segment) => segment.id),
|
|
1763
|
+
);
|
|
1764
|
+
const stillHere = rootRef.current === targetRoot && rootSessionRef.current === targetSession;
|
|
1765
|
+
if (ids.size > 0 && stillHere) {
|
|
1766
|
+
const archivedAt = new Date().toISOString();
|
|
1767
|
+
const next = segmentsRef.current.map((segment) =>
|
|
1768
|
+
(ids.has(segment.id) ? { ...segment, archived: true, archivedAt } : segment));
|
|
1769
|
+
setSelection(new Set([...selectedRef.current].filter((id) => !ids.has(id))));
|
|
1770
|
+
commitSegments(next);
|
|
1771
|
+
}
|
|
1772
|
+
} catch (reason) {
|
|
1773
|
+
showToast(`${t('sendFailed')}: ${String(reason?.message ?? reason)}`);
|
|
1774
|
+
} finally {
|
|
1775
|
+
sendingRef.current = false;
|
|
1776
|
+
setSending(false);
|
|
1777
|
+
}
|
|
1778
|
+
};
|
|
1779
|
+
|
|
1780
|
+
const copyAll = async () => {
|
|
1781
|
+
const value = mergedText(segmentsRef.current);
|
|
1782
|
+
if (value === '') {
|
|
1783
|
+
showToast(t('insertEmpty'));
|
|
1784
|
+
return;
|
|
1785
|
+
}
|
|
697
1786
|
showToast((await copyText(value)) ? t('copied') : t('copyFailed'));
|
|
698
1787
|
};
|
|
699
1788
|
|
|
700
|
-
|
|
1789
|
+
// UI actions ----------------------------------------------------------
|
|
1790
|
+
const toggleCollapse = () => {
|
|
701
1791
|
setUi((previous) => clampUi({ ...previous, minimized: !previous.minimized }));
|
|
702
1792
|
};
|
|
703
1793
|
|
|
@@ -705,11 +1795,40 @@ window.__ModuleLoader__.load({
|
|
|
705
1795
|
setUi((previous) => clampUi({ ...previous, hidden: !previous.hidden }));
|
|
706
1796
|
};
|
|
707
1797
|
|
|
708
|
-
const
|
|
709
|
-
|
|
1798
|
+
const toggleArchive = () => {
|
|
1799
|
+
const open = ui.showArchive !== true;
|
|
1800
|
+
setUi((previous) => ({ ...previous, showArchive: open }));
|
|
1801
|
+
if (open) {
|
|
1802
|
+
setTimeout(() => {
|
|
1803
|
+
const list = listRef.current;
|
|
1804
|
+
if (list !== null && list !== undefined) list.scrollTop = list.scrollHeight;
|
|
1805
|
+
}, 260);
|
|
1806
|
+
}
|
|
1807
|
+
};
|
|
1808
|
+
|
|
1809
|
+
const onSegmentChange = (event, id) => {
|
|
1810
|
+
const target = event.currentTarget ?? event.target;
|
|
1811
|
+
autoGrow(target);
|
|
1812
|
+
updateSegmentText(id, event.target?.value ?? '');
|
|
1813
|
+
};
|
|
1814
|
+
|
|
1815
|
+
const onSegmentKeyDown = (event, id) => {
|
|
1816
|
+
if (event.isComposing === true || event.nativeEvent?.isComposing === true) return;
|
|
1817
|
+
const chord = event.ctrlKey || event.metaKey;
|
|
1818
|
+
if (chord && event.key === 'Enter') {
|
|
710
1819
|
event.preventDefault();
|
|
711
|
-
|
|
1820
|
+
const current = segmentsRef.current.find((segment) => segment.id === id);
|
|
1821
|
+
if (current === undefined) return;
|
|
1822
|
+
const active = segmentsRef.current.filter((segment) => segment.archived !== true);
|
|
1823
|
+
const list = selectedRef.current.size > 0
|
|
1824
|
+
? active.filter((segment) => selectedRef.current.has(segment.id))
|
|
1825
|
+
: [current];
|
|
1826
|
+
insertSegments(list);
|
|
1827
|
+
return;
|
|
712
1828
|
}
|
|
1829
|
+
if (event.key !== 'Enter' || event.shiftKey || event.altKey || chord) return;
|
|
1830
|
+
event.preventDefault();
|
|
1831
|
+
splitSegment(id, event.currentTarget ?? event.target);
|
|
713
1832
|
};
|
|
714
1833
|
|
|
715
1834
|
const onHeaderPointerDown = (event) => {
|
|
@@ -722,9 +1841,8 @@ window.__ModuleLoader__.load({
|
|
|
722
1841
|
const move = (moveEvent) => {
|
|
723
1842
|
const vw = window.innerWidth || 1280;
|
|
724
1843
|
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));
|
|
1844
|
+
const height = ui.minimized ? HEAD_H : ui.height;
|
|
1845
|
+
const x = Math.min(Math.max(8, start.x + moveEvent.clientX - start.px), Math.max(8, vw - ui.width - 8));
|
|
728
1846
|
const y = Math.min(Math.max(8, start.y + moveEvent.clientY - start.py), Math.max(8, vh - height - 8));
|
|
729
1847
|
setUi((previous) => ({ ...previous, x, y }));
|
|
730
1848
|
};
|
|
@@ -740,25 +1858,56 @@ window.__ModuleLoader__.load({
|
|
|
740
1858
|
event.preventDefault?.();
|
|
741
1859
|
};
|
|
742
1860
|
|
|
1861
|
+
const onResizePointerDown = (event) => {
|
|
1862
|
+
if (ui.minimized) return;
|
|
1863
|
+
if (event.button !== undefined && event.button !== 0) return;
|
|
1864
|
+
event.preventDefault?.();
|
|
1865
|
+
event.stopPropagation?.();
|
|
1866
|
+
const start = { px: event.clientX, py: event.clientY, width: ui.width, height: ui.height };
|
|
1867
|
+
resizeRef.current = start;
|
|
1868
|
+
setResizing(true);
|
|
1869
|
+
const move = (moveEvent) => {
|
|
1870
|
+
const vw = window.innerWidth || 1280;
|
|
1871
|
+
const vh = window.innerHeight || 800;
|
|
1872
|
+
const width = Math.min(Math.max(MIN_W, start.width + moveEvent.clientX - start.px), Math.max(MIN_W, vw - 16));
|
|
1873
|
+
const height = Math.min(Math.max(MIN_H, start.height + moveEvent.clientY - start.py), Math.max(MIN_H, vh - 16));
|
|
1874
|
+
setUi((previous) => {
|
|
1875
|
+
const maxX = Math.max(8, vw - width - 8);
|
|
1876
|
+
const maxY = Math.max(8, vh - height - 8);
|
|
1877
|
+
return {
|
|
1878
|
+
...previous,
|
|
1879
|
+
width,
|
|
1880
|
+
height,
|
|
1881
|
+
x: typeof previous.x === 'number' ? Math.min(previous.x, maxX) : previous.x,
|
|
1882
|
+
y: typeof previous.y === 'number' ? Math.min(previous.y, maxY) : previous.y,
|
|
1883
|
+
};
|
|
1884
|
+
});
|
|
1885
|
+
};
|
|
1886
|
+
const stop = () => {
|
|
1887
|
+
resizeRef.current = null;
|
|
1888
|
+
setResizing(false);
|
|
1889
|
+
window.removeEventListener('pointermove', move);
|
|
1890
|
+
window.removeEventListener('pointerup', stop);
|
|
1891
|
+
window.removeEventListener('pointercancel', stop);
|
|
1892
|
+
};
|
|
1893
|
+
window.addEventListener('pointermove', move);
|
|
1894
|
+
window.addEventListener('pointerup', stop);
|
|
1895
|
+
window.addEventListener('pointercancel', stop);
|
|
1896
|
+
};
|
|
1897
|
+
|
|
743
1898
|
// Render --------------------------------------------------------------
|
|
744
1899
|
if (binding === null || present !== true) return null;
|
|
745
1900
|
|
|
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);
|
|
1901
|
+
const activeSegments = segments.filter((segment) => segment.archived !== true);
|
|
1902
|
+
const archivedSegments = segments.filter((segment) => segment.archived === true);
|
|
1903
|
+
const selectedActive = activeSegments.filter((segment) => selected.has(segment.id));
|
|
1904
|
+
const selectedCount = selectedActive.length;
|
|
1905
|
+
const allSelected = activeSegments.length > 0 && selectedCount === activeSegments.length;
|
|
1906
|
+
const totalChars = segments.reduce((sum, segment) => sum + segment.text.length, 0);
|
|
1907
|
+
const displayWidth = ui.width;
|
|
1908
|
+
const displayHeight = ui.minimized ? HEAD_H : ui.height;
|
|
1909
|
+
const fallbackX = Math.max(8, ((typeof window !== 'undefined' && window.innerWidth) || 1280) - displayWidth - 24);
|
|
1910
|
+
const fallbackY = Math.max(8, ((typeof window !== 'undefined' && window.innerHeight) || 800) - displayHeight - 24);
|
|
762
1911
|
const left = typeof ui.x === 'number' ? ui.x : fallbackX;
|
|
763
1912
|
const top = typeof ui.y === 'number' ? ui.y : fallbackY;
|
|
764
1913
|
|
|
@@ -766,119 +1915,602 @@ window.__ModuleLoader__.load({
|
|
|
766
1915
|
if (status === 'loading') statusText = t('loading');
|
|
767
1916
|
else if (status === 'saving') statusText = t('saving');
|
|
768
1917
|
else if (status === 'dirty') statusText = t('unsaved');
|
|
769
|
-
else if (status === 'error') statusText = t('saveFailed');
|
|
1918
|
+
else if (status === 'error') statusText = loadFailedRef.current ? t('loadFailed') : t('saveFailed');
|
|
770
1919
|
else if (status === 'saved') {
|
|
771
1920
|
const clock = clockLabel(savedAt);
|
|
772
1921
|
statusText = clock === '' ? t('saved') : `${t('saved')} · ${clock}`;
|
|
773
1922
|
} else statusText = t('empty');
|
|
774
1923
|
|
|
775
|
-
|
|
776
|
-
'
|
|
777
|
-
{
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
},
|
|
1924
|
+
const archiveButtonTitle = ui.showArchive
|
|
1925
|
+
? t('hideArchive', { n: archivedSegments.length })
|
|
1926
|
+
: t('showArchive', { n: archivedSegments.length });
|
|
1927
|
+
|
|
1928
|
+
const header = h(
|
|
1929
|
+
'div',
|
|
1930
|
+
{ className: 'dsh-np-head', onPointerDown: onHeaderPointerDown },
|
|
1931
|
+
h('span', { className: 'dsh-np-dot', 'data-state': status }),
|
|
1932
|
+
h('span', { className: 'dsh-np-title' }, t('title')),
|
|
1933
|
+
h('span', { className: 'dsh-np-workspace', title: binding.cwd }, workspaceName(binding.cwd)),
|
|
1934
|
+
h('span', { className: 'dsh-np-spacer' }),
|
|
1935
|
+
h(
|
|
1936
|
+
'button',
|
|
1937
|
+
{
|
|
1938
|
+
type: 'button',
|
|
1939
|
+
className: 'dsh-np-btn',
|
|
1940
|
+
title: t('copy'),
|
|
1941
|
+
'aria-label': t('copy'),
|
|
1942
|
+
disabled: totalChars === 0,
|
|
1943
|
+
onClick: copyAll,
|
|
1944
|
+
},
|
|
1945
|
+
h(CopyIcon, { size: 15 }),
|
|
1946
|
+
),
|
|
1947
|
+
h(
|
|
1948
|
+
'button',
|
|
1949
|
+
{
|
|
1950
|
+
type: 'button',
|
|
1951
|
+
className: 'dsh-np-btn',
|
|
1952
|
+
'data-active': ui.showArchive ? 'true' : 'false',
|
|
1953
|
+
'aria-pressed': ui.showArchive ? 'true' : 'false',
|
|
1954
|
+
title: archiveButtonTitle,
|
|
1955
|
+
'aria-label': archiveButtonTitle,
|
|
1956
|
+
onClick: toggleArchive,
|
|
1957
|
+
},
|
|
1958
|
+
h(ArchiveIcon, { size: 15 }),
|
|
1959
|
+
archivedSegments.length > 0
|
|
1960
|
+
? h('span', { className: 'dsh-np-badge' }, String(archivedSegments.length))
|
|
1961
|
+
: null,
|
|
1962
|
+
),
|
|
786
1963
|
h(
|
|
1964
|
+
'button',
|
|
1965
|
+
{
|
|
1966
|
+
type: 'button',
|
|
1967
|
+
className: 'dsh-np-btn',
|
|
1968
|
+
title: t('addSegment'),
|
|
1969
|
+
'aria-label': t('addSegment'),
|
|
1970
|
+
onClick: addSegment,
|
|
1971
|
+
},
|
|
1972
|
+
h(PlusIcon, { size: 16 }),
|
|
1973
|
+
),
|
|
1974
|
+
h(
|
|
1975
|
+
'button',
|
|
1976
|
+
{
|
|
1977
|
+
type: 'button',
|
|
1978
|
+
className: 'dsh-np-btn',
|
|
1979
|
+
title: ui.minimized ? t('expand') : t('collapse'),
|
|
1980
|
+
'aria-label': ui.minimized ? t('expand') : t('collapse'),
|
|
1981
|
+
onClick: toggleCollapse,
|
|
1982
|
+
},
|
|
1983
|
+
h(ui.minimized ? ChevronsExpandIcon : ChevronsCollapseIcon, { size: 15 }),
|
|
1984
|
+
),
|
|
1985
|
+
h(
|
|
1986
|
+
'button',
|
|
1987
|
+
{
|
|
1988
|
+
type: 'button',
|
|
1989
|
+
className: 'dsh-np-btn',
|
|
1990
|
+
title: t('minimize'),
|
|
1991
|
+
'aria-label': t('minimize'),
|
|
1992
|
+
onClick: toggleHidden,
|
|
1993
|
+
},
|
|
1994
|
+
h(MinimizeIcon, { size: 15 }),
|
|
1995
|
+
),
|
|
1996
|
+
);
|
|
1997
|
+
|
|
1998
|
+
const tools = h(
|
|
1999
|
+
'div',
|
|
2000
|
+
{ className: 'dsh-np-tools' },
|
|
2001
|
+
h(
|
|
2002
|
+
'label',
|
|
2003
|
+
{ className: 'dsh-np-selectall', title: t('selectAll') },
|
|
2004
|
+
h('input', {
|
|
2005
|
+
type: 'checkbox',
|
|
2006
|
+
checked: allSelected,
|
|
2007
|
+
disabled: activeSegments.length === 0,
|
|
2008
|
+
'aria-label': t('selectAll'),
|
|
2009
|
+
onChange: toggleSelectAll,
|
|
2010
|
+
}),
|
|
2011
|
+
h('span', { className: 'dsh-np-selcount' },
|
|
2012
|
+
selectedCount > 0 ? t('selected', { n: selectedCount }) : t('selectHint')),
|
|
2013
|
+
),
|
|
2014
|
+
h('span', { className: 'dsh-np-spacer' }),
|
|
2015
|
+
h(
|
|
2016
|
+
'button',
|
|
2017
|
+
{
|
|
2018
|
+
type: 'button',
|
|
2019
|
+
className: 'dsh-np-btn',
|
|
2020
|
+
title: selectedCount > 0 ? t('insertSelected', { n: selectedCount }) : t('insert'),
|
|
2021
|
+
'aria-label': t('insert'),
|
|
2022
|
+
disabled: selectedCount === 0,
|
|
2023
|
+
onClick: () => insertSegments(selectedActive),
|
|
2024
|
+
},
|
|
2025
|
+
h(InsertIcon, { size: 14 }),
|
|
2026
|
+
),
|
|
2027
|
+
h(
|
|
2028
|
+
'button',
|
|
2029
|
+
{
|
|
2030
|
+
type: 'button',
|
|
2031
|
+
className: 'dsh-np-btn',
|
|
2032
|
+
title: selectedCount > 0 ? t('replaceSelected', { n: selectedCount }) : t('replace'),
|
|
2033
|
+
'aria-label': t('replace'),
|
|
2034
|
+
disabled: selectedCount === 0,
|
|
2035
|
+
onClick: () => replaceSegments(selectedActive),
|
|
2036
|
+
},
|
|
2037
|
+
h(ReplaceIcon, { size: 14 }),
|
|
2038
|
+
),
|
|
2039
|
+
h(
|
|
2040
|
+
'button',
|
|
2041
|
+
{
|
|
2042
|
+
type: 'button',
|
|
2043
|
+
className: 'dsh-np-btn',
|
|
2044
|
+
'data-accent': 'true',
|
|
2045
|
+
title: selectedCount > 0
|
|
2046
|
+
? (sending ? t('sending') : t('sendSelected', { n: selectedCount }))
|
|
2047
|
+
: t('send'),
|
|
2048
|
+
'aria-label': t('send'),
|
|
2049
|
+
disabled: selectedCount === 0 || sending,
|
|
2050
|
+
onClick: () => sendSegments(selectedActive),
|
|
2051
|
+
},
|
|
2052
|
+
h(SendIcon, { size: 14 }),
|
|
2053
|
+
),
|
|
2054
|
+
h(
|
|
2055
|
+
'button',
|
|
2056
|
+
{
|
|
2057
|
+
type: 'button',
|
|
2058
|
+
className: 'dsh-np-btn',
|
|
2059
|
+
title: selectedCount > 0 ? t('archiveSelected', { n: selectedCount }) : t('archive'),
|
|
2060
|
+
'aria-label': t('archive'),
|
|
2061
|
+
disabled: selectedCount === 0,
|
|
2062
|
+
onClick: archiveSelected,
|
|
2063
|
+
},
|
|
2064
|
+
h(ArchiveIcon, { size: 14 }),
|
|
2065
|
+
),
|
|
2066
|
+
selectedCount > 0
|
|
2067
|
+
? h(
|
|
2068
|
+
'button',
|
|
2069
|
+
{
|
|
2070
|
+
type: 'button',
|
|
2071
|
+
className: 'dsh-np-btn',
|
|
2072
|
+
title: t('clearSelection'),
|
|
2073
|
+
'aria-label': t('clearSelection'),
|
|
2074
|
+
onClick: () => setSelection(new Set()),
|
|
2075
|
+
},
|
|
2076
|
+
h(CloseIcon, { size: 13 }),
|
|
2077
|
+
)
|
|
2078
|
+
: null,
|
|
2079
|
+
);
|
|
2080
|
+
|
|
2081
|
+
const totalImages = segments.reduce(
|
|
2082
|
+
(sum, segment) => sum + (Array.isArray(segment.images) ? segment.images.length : 0), 0);
|
|
2083
|
+
|
|
2084
|
+
const renderMedia = (segment, options = {}) => {
|
|
2085
|
+
const images = Array.isArray(segment.images) ? segment.images : [];
|
|
2086
|
+
const pending = options.pending === true;
|
|
2087
|
+
if (images.length === 0 && !pending) return null;
|
|
2088
|
+
return h(
|
|
787
2089
|
'div',
|
|
788
|
-
{ className: 'dsh-np-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
2090
|
+
{ className: 'dsh-np-medias' },
|
|
2091
|
+
pending
|
|
2092
|
+
? h(
|
|
2093
|
+
'div',
|
|
2094
|
+
{ key: 'pending', className: 'dsh-np-img', 'data-pending': 'true' },
|
|
2095
|
+
h('span', { className: 'dsh-np-img-spinner' }),
|
|
2096
|
+
h('span', { className: 'dsh-np-img-name' }, t('imageUploading')),
|
|
2097
|
+
)
|
|
2098
|
+
: null,
|
|
2099
|
+
images.map((image, imageIndex) => h(
|
|
2100
|
+
'div',
|
|
2101
|
+
{ key: `${imageIndex}-${image.path}`, className: 'dsh-np-img', title: image.path },
|
|
2102
|
+
h(
|
|
2103
|
+
'button',
|
|
2104
|
+
{
|
|
2105
|
+
type: 'button',
|
|
2106
|
+
className: 'dsh-np-img-open',
|
|
2107
|
+
title: `${t('openImage')}: ${image.path}`,
|
|
2108
|
+
'aria-label': t('openImage'),
|
|
2109
|
+
onClick: () => openImage(image),
|
|
2110
|
+
},
|
|
2111
|
+
h('img', {
|
|
2112
|
+
src: imageUrl(image.path, binding.cwd),
|
|
2113
|
+
alt: image.name,
|
|
2114
|
+
loading: 'lazy',
|
|
2115
|
+
decoding: 'async',
|
|
2116
|
+
draggable: false,
|
|
2117
|
+
}),
|
|
2118
|
+
),
|
|
2119
|
+
h('span', { className: 'dsh-np-img-name', title: image.path }, image.name),
|
|
2120
|
+
options.readOnly === true
|
|
2121
|
+
? null
|
|
2122
|
+
: h(
|
|
2123
|
+
'button',
|
|
2124
|
+
{
|
|
2125
|
+
type: 'button',
|
|
2126
|
+
className: 'dsh-np-img-remove',
|
|
2127
|
+
title: t('removeImage'),
|
|
2128
|
+
'aria-label': t('removeImage'),
|
|
2129
|
+
onClick: () => removeImage(segment.id, image.path),
|
|
2130
|
+
},
|
|
2131
|
+
h(CloseIcon, { size: 11 }),
|
|
2132
|
+
),
|
|
2133
|
+
)),
|
|
2134
|
+
);
|
|
2135
|
+
};
|
|
2136
|
+
|
|
2137
|
+
const activeCards = activeSegments.map((segment, index) => {
|
|
2138
|
+
const images = Array.isArray(segment.images) ? segment.images : [];
|
|
2139
|
+
const empty = segment.text.trim() === '' && images.length === 0;
|
|
2140
|
+
return h(
|
|
2141
|
+
'article',
|
|
2142
|
+
{
|
|
2143
|
+
key: segment.id,
|
|
2144
|
+
className: 'dsh-np-seg',
|
|
2145
|
+
'data-seg-id': segment.id,
|
|
2146
|
+
'data-archived': 'false',
|
|
2147
|
+
'data-selected': selected.has(segment.id) ? 'true' : 'false',
|
|
2148
|
+
},
|
|
2149
|
+
h('input', {
|
|
2150
|
+
key: 'file-input',
|
|
2151
|
+
ref: (element) => {
|
|
2152
|
+
if (element === null || element === undefined) {
|
|
2153
|
+
fileInputRefs.current.delete(segment.id);
|
|
2154
|
+
return;
|
|
2155
|
+
}
|
|
2156
|
+
fileInputRefs.current.set(segment.id, element);
|
|
803
2157
|
},
|
|
804
|
-
|
|
805
|
-
|
|
2158
|
+
className: 'dsh-np-file',
|
|
2159
|
+
type: 'file',
|
|
2160
|
+
accept: 'image/png,image/jpeg,image/webp,image/gif',
|
|
2161
|
+
multiple: true,
|
|
2162
|
+
tabIndex: -1,
|
|
2163
|
+
'aria-hidden': 'true',
|
|
2164
|
+
onChange: (event) => onFileInputChange(event, segment.id),
|
|
2165
|
+
}),
|
|
806
2166
|
h(
|
|
807
|
-
'
|
|
808
|
-
{
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
title: t('
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
2167
|
+
'div',
|
|
2168
|
+
{ className: 'dsh-np-seg-head' },
|
|
2169
|
+
h(
|
|
2170
|
+
'label',
|
|
2171
|
+
{ className: 'dsh-np-check', title: t('selectSegment') },
|
|
2172
|
+
h('input', {
|
|
2173
|
+
type: 'checkbox',
|
|
2174
|
+
checked: selected.has(segment.id),
|
|
2175
|
+
'aria-label': t('selectSegment'),
|
|
2176
|
+
onChange: () => toggleSelected(segment.id),
|
|
2177
|
+
}),
|
|
2178
|
+
),
|
|
2179
|
+
h('span', { className: 'dsh-np-index' }, String(index + 1).padStart(2, '0')),
|
|
2180
|
+
h('span', { className: 'dsh-np-spacer' }),
|
|
2181
|
+
h(
|
|
2182
|
+
'button',
|
|
2183
|
+
{
|
|
2184
|
+
type: 'button',
|
|
2185
|
+
className: 'dsh-np-segbtn',
|
|
2186
|
+
title: t('addImage'),
|
|
2187
|
+
'aria-label': t('addImage'),
|
|
2188
|
+
onClick: () => triggerFilePicker(segment.id),
|
|
2189
|
+
},
|
|
2190
|
+
h(ImageIcon, { size: 14 }),
|
|
2191
|
+
),
|
|
2192
|
+
h(
|
|
2193
|
+
'button',
|
|
2194
|
+
{
|
|
2195
|
+
type: 'button',
|
|
2196
|
+
className: 'dsh-np-segbtn',
|
|
2197
|
+
title: t('insertSegment'),
|
|
2198
|
+
'aria-label': t('insertSegment'),
|
|
2199
|
+
disabled: empty,
|
|
2200
|
+
onClick: () => insertSegments([segment]),
|
|
2201
|
+
},
|
|
2202
|
+
h(InsertIcon, { size: 14 }),
|
|
2203
|
+
),
|
|
2204
|
+
h(
|
|
2205
|
+
'button',
|
|
2206
|
+
{
|
|
2207
|
+
type: 'button',
|
|
2208
|
+
className: 'dsh-np-segbtn',
|
|
2209
|
+
title: t('replaceSegment'),
|
|
2210
|
+
'aria-label': t('replaceSegment'),
|
|
2211
|
+
disabled: empty,
|
|
2212
|
+
onClick: () => replaceSegments([segment]),
|
|
2213
|
+
},
|
|
2214
|
+
h(ReplaceIcon, { size: 14 }),
|
|
2215
|
+
),
|
|
2216
|
+
h(
|
|
2217
|
+
'button',
|
|
2218
|
+
{
|
|
2219
|
+
type: 'button',
|
|
2220
|
+
className: 'dsh-np-segbtn',
|
|
2221
|
+
title: t('sendSegment'),
|
|
2222
|
+
'aria-label': t('sendSegment'),
|
|
2223
|
+
disabled: empty || sending,
|
|
2224
|
+
onClick: () => sendSegments([segment]),
|
|
2225
|
+
},
|
|
2226
|
+
h(SendIcon, { size: 14 }),
|
|
2227
|
+
),
|
|
2228
|
+
h(
|
|
2229
|
+
'button',
|
|
2230
|
+
{
|
|
2231
|
+
type: 'button',
|
|
2232
|
+
className: 'dsh-np-segbtn',
|
|
2233
|
+
title: t('archiveSegment'),
|
|
2234
|
+
'aria-label': t('archiveSegment'),
|
|
2235
|
+
onClick: () => archiveSegment(segment.id),
|
|
2236
|
+
},
|
|
2237
|
+
h(ArchiveIcon, { size: 14 }),
|
|
2238
|
+
),
|
|
817
2239
|
),
|
|
818
2240
|
h(
|
|
819
|
-
'
|
|
2241
|
+
'div',
|
|
820
2242
|
{
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
2243
|
+
className: 'dsh-np-editor',
|
|
2244
|
+
'data-drag': dragSegment === segment.id ? 'true' : 'false',
|
|
2245
|
+
onPaste: (event) => onSegmentPaste(event, segment.id),
|
|
2246
|
+
onDrop: (event) => onSegmentDrop(event, segment.id),
|
|
2247
|
+
onDragOver: (event) => onSegmentDragOver(event, segment.id),
|
|
2248
|
+
onDragLeave: (event) => onSegmentDragLeave(event, segment.id),
|
|
826
2249
|
},
|
|
827
|
-
|
|
2250
|
+
renderMedia(segment, { pending: (uploading[segment.id] ?? 0) > 0 }),
|
|
2251
|
+
h('textarea', {
|
|
2252
|
+
ref: (element) => {
|
|
2253
|
+
if (element === null || element === undefined) {
|
|
2254
|
+
textareaRefs.current.delete(segment.id);
|
|
2255
|
+
return;
|
|
2256
|
+
}
|
|
2257
|
+
textareaRefs.current.set(segment.id, element);
|
|
2258
|
+
autoGrow(element);
|
|
2259
|
+
},
|
|
2260
|
+
className: 'dsh-np-seg-text',
|
|
2261
|
+
rows: 1,
|
|
2262
|
+
value: segment.text,
|
|
2263
|
+
placeholder: index === 0 ? t('placeholder') : t('segmentPlaceholder'),
|
|
2264
|
+
spellCheck: false,
|
|
2265
|
+
maxLength: MAX_SEGMENT_CHARS,
|
|
2266
|
+
'aria-label': t('editor'),
|
|
2267
|
+
onChange: (event) => onSegmentChange(event, segment.id),
|
|
2268
|
+
onKeyDown: (event) => onSegmentKeyDown(event, segment.id),
|
|
2269
|
+
onBlur: () => {
|
|
2270
|
+
if (dirtyRef.current) saveNowRef.current?.();
|
|
2271
|
+
},
|
|
2272
|
+
}),
|
|
828
2273
|
),
|
|
829
|
-
|
|
830
|
-
|
|
2274
|
+
);
|
|
2275
|
+
});
|
|
2276
|
+
|
|
2277
|
+
const activeEmpty = activeSegments.length === 0
|
|
2278
|
+
? h(
|
|
2279
|
+
'div',
|
|
2280
|
+
{ key: 'active-empty', className: 'dsh-np-empty' },
|
|
2281
|
+
h('span', null, t('noActive')),
|
|
2282
|
+
h(
|
|
2283
|
+
'button',
|
|
2284
|
+
{ type: 'button', className: 'dsh-np-empty-add', onClick: addSegment },
|
|
2285
|
+
h(PlusIcon, { size: 14 }),
|
|
2286
|
+
h('span', null, t('addSegment')),
|
|
2287
|
+
),
|
|
2288
|
+
)
|
|
2289
|
+
: null;
|
|
2290
|
+
|
|
2291
|
+
const archiveSection = archivedSegments.length > 0 || ui.showArchive
|
|
2292
|
+
? h(
|
|
2293
|
+
'div',
|
|
831
2294
|
{
|
|
832
|
-
|
|
833
|
-
className: 'dsh-np-
|
|
834
|
-
|
|
835
|
-
'aria-
|
|
836
|
-
onClick: toggleHidden,
|
|
2295
|
+
key: 'archive',
|
|
2296
|
+
className: 'dsh-np-archive',
|
|
2297
|
+
'data-open': ui.showArchive ? 'true' : 'false',
|
|
2298
|
+
'aria-hidden': ui.showArchive ? undefined : 'true',
|
|
837
2299
|
},
|
|
838
|
-
h(
|
|
839
|
-
),
|
|
840
|
-
),
|
|
841
|
-
ui.minimized
|
|
842
|
-
? null
|
|
843
|
-
: h(
|
|
2300
|
+
h(
|
|
844
2301
|
'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
|
-
}),
|
|
2302
|
+
{ className: 'dsh-np-archive-inner' },
|
|
861
2303
|
h(
|
|
862
2304
|
'div',
|
|
863
|
-
{ className: 'dsh-np-
|
|
864
|
-
h(
|
|
865
|
-
|
|
866
|
-
'
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
status === 'error'
|
|
2305
|
+
{ className: 'dsh-np-archive-head' },
|
|
2306
|
+
h(ArchiveIcon, { size: 13 }),
|
|
2307
|
+
h('span', { className: 'dsh-np-archive-label' },
|
|
2308
|
+
t('archiveCount', { n: archivedSegments.length })),
|
|
2309
|
+
h('span', { className: 'dsh-np-spacer' }),
|
|
2310
|
+
archivedSegments.length > 0
|
|
870
2311
|
? h('button', {
|
|
871
2312
|
type: 'button',
|
|
872
|
-
className: 'dsh-np-
|
|
873
|
-
|
|
874
|
-
|
|
2313
|
+
className: 'dsh-np-link',
|
|
2314
|
+
title: t('restoreAll'),
|
|
2315
|
+
onClick: restoreAll,
|
|
2316
|
+
}, t('restoreAll'))
|
|
2317
|
+
: null,
|
|
2318
|
+
archivedSegments.length > 0
|
|
2319
|
+
? h('button', {
|
|
2320
|
+
type: 'button',
|
|
2321
|
+
className: 'dsh-np-link',
|
|
2322
|
+
'data-danger': 'true',
|
|
2323
|
+
title: t('clearArchive'),
|
|
2324
|
+
onClick: clearArchive,
|
|
2325
|
+
}, t('clearArchive'))
|
|
875
2326
|
: 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
2327
|
),
|
|
879
|
-
|
|
2328
|
+
archivedSegments.length === 0
|
|
2329
|
+
? h('div', { className: 'dsh-np-empty-small' }, t('archiveEmpty'))
|
|
2330
|
+
: h(
|
|
2331
|
+
'div',
|
|
2332
|
+
{ className: 'dsh-np-archive-list' },
|
|
2333
|
+
archivedSegments.map((segment) => h(
|
|
2334
|
+
'article',
|
|
2335
|
+
{
|
|
2336
|
+
key: segment.id,
|
|
2337
|
+
className: 'dsh-np-seg',
|
|
2338
|
+
'data-seg-id': segment.id,
|
|
2339
|
+
'data-archived': 'true',
|
|
2340
|
+
},
|
|
2341
|
+
h(
|
|
2342
|
+
'div',
|
|
2343
|
+
{ className: 'dsh-np-seg-head' },
|
|
2344
|
+
h('span', { className: 'dsh-np-archived-chip' }, t('archived')),
|
|
2345
|
+
h('span', { className: 'dsh-np-spacer' }),
|
|
2346
|
+
h(
|
|
2347
|
+
'button',
|
|
2348
|
+
{
|
|
2349
|
+
type: 'button',
|
|
2350
|
+
className: 'dsh-np-segbtn',
|
|
2351
|
+
title: t('insertSegment'),
|
|
2352
|
+
'aria-label': t('insertSegment'),
|
|
2353
|
+
disabled: segment.text.trim() === ''
|
|
2354
|
+
&& !(Array.isArray(segment.images) && segment.images.length > 0),
|
|
2355
|
+
onClick: () => insertSegments([segment]),
|
|
2356
|
+
},
|
|
2357
|
+
h(InsertIcon, { size: 14 }),
|
|
2358
|
+
),
|
|
2359
|
+
h(
|
|
2360
|
+
'button',
|
|
2361
|
+
{
|
|
2362
|
+
type: 'button',
|
|
2363
|
+
className: 'dsh-np-segbtn',
|
|
2364
|
+
title: t('replaceSegment'),
|
|
2365
|
+
'aria-label': t('replaceSegment'),
|
|
2366
|
+
disabled: segment.text.trim() === ''
|
|
2367
|
+
&& !(Array.isArray(segment.images) && segment.images.length > 0),
|
|
2368
|
+
onClick: () => replaceSegments([segment]),
|
|
2369
|
+
},
|
|
2370
|
+
h(ReplaceIcon, { size: 14 }),
|
|
2371
|
+
),
|
|
2372
|
+
h(
|
|
2373
|
+
'button',
|
|
2374
|
+
{
|
|
2375
|
+
type: 'button',
|
|
2376
|
+
className: 'dsh-np-segbtn',
|
|
2377
|
+
title: t('restoreSegment'),
|
|
2378
|
+
'aria-label': t('restoreSegment'),
|
|
2379
|
+
onClick: () => restoreSegment(segment.id),
|
|
2380
|
+
},
|
|
2381
|
+
h(RestoreIcon, { size: 14 }),
|
|
2382
|
+
),
|
|
2383
|
+
h(
|
|
2384
|
+
'button',
|
|
2385
|
+
{
|
|
2386
|
+
type: 'button',
|
|
2387
|
+
className: 'dsh-np-segbtn',
|
|
2388
|
+
'data-danger': 'true',
|
|
2389
|
+
title: t('deleteSegment'),
|
|
2390
|
+
'aria-label': t('deleteSegment'),
|
|
2391
|
+
onClick: () => deleteArchived(segment.id),
|
|
2392
|
+
},
|
|
2393
|
+
h(TrashIcon, { size: 14 }),
|
|
2394
|
+
),
|
|
2395
|
+
),
|
|
2396
|
+
h('div', { className: 'dsh-np-seg-archived-text' },
|
|
2397
|
+
segment.text === ''
|
|
2398
|
+
? (Array.isArray(segment.images) && segment.images.length > 0 ? null : t('emptySegment'))
|
|
2399
|
+
: segment.text),
|
|
2400
|
+
renderMedia(segment, { readOnly: true }),
|
|
2401
|
+
)),
|
|
2402
|
+
),
|
|
880
2403
|
),
|
|
2404
|
+
)
|
|
2405
|
+
: null;
|
|
2406
|
+
|
|
2407
|
+
const listContent = status === 'loading'
|
|
2408
|
+
? h('div', { key: 'loading', className: 'dsh-np-empty' }, t('loading'))
|
|
2409
|
+
: [activeCards, activeEmpty, archiveSection];
|
|
2410
|
+
|
|
2411
|
+
const footer = h(
|
|
2412
|
+
'div',
|
|
2413
|
+
{ className: 'dsh-np-foot' },
|
|
2414
|
+
h('span', {
|
|
2415
|
+
className: 'dsh-np-status',
|
|
2416
|
+
'data-state': status,
|
|
2417
|
+
title: error === '' ? undefined : error,
|
|
2418
|
+
}, statusText),
|
|
2419
|
+
status === 'error'
|
|
2420
|
+
? h('button', {
|
|
2421
|
+
type: 'button',
|
|
2422
|
+
className: 'dsh-np-retry',
|
|
2423
|
+
onClick: () => {
|
|
2424
|
+
if (loadFailedRef.current) reloadRef.current?.();
|
|
2425
|
+
else saveNowRef.current?.({ force: true });
|
|
2426
|
+
},
|
|
2427
|
+
}, t('retry'))
|
|
2428
|
+
: null,
|
|
2429
|
+
h('span', { className: 'dsh-np-count' },
|
|
2430
|
+
`${t('chars', { n: totalChars })} · ${t('segments', { n: activeSegments.length })}`
|
|
2431
|
+
+ (totalImages > 0 ? ` · ${t('images', { n: totalImages })}` : '')),
|
|
2432
|
+
h('span', { className: 'dsh-np-hint', title: t('hint') }, t('hint')),
|
|
881
2433
|
);
|
|
2434
|
+
|
|
2435
|
+
const toastNode = toast === null
|
|
2436
|
+
? null
|
|
2437
|
+
: h(
|
|
2438
|
+
'div',
|
|
2439
|
+
{ className: 'dsh-np-toast', role: 'status' },
|
|
2440
|
+
h('span', { className: 'dsh-np-toast-text' }, toast.message),
|
|
2441
|
+
toast.action !== null && toast.action !== undefined
|
|
2442
|
+
? h('button', {
|
|
2443
|
+
type: 'button',
|
|
2444
|
+
className: 'dsh-np-toast-action',
|
|
2445
|
+
onClick: toast.action.onClick,
|
|
2446
|
+
}, toast.action.label)
|
|
2447
|
+
: null,
|
|
2448
|
+
);
|
|
2449
|
+
|
|
2450
|
+
const body = h(
|
|
2451
|
+
'div',
|
|
2452
|
+
{
|
|
2453
|
+
className: 'dsh-np-body',
|
|
2454
|
+
'aria-hidden': ui.minimized ? 'true' : undefined,
|
|
2455
|
+
},
|
|
2456
|
+
tools,
|
|
2457
|
+
h('div', { className: 'dsh-np-list', ref: listRef }, listContent),
|
|
2458
|
+
footer,
|
|
2459
|
+
toastNode,
|
|
2460
|
+
);
|
|
2461
|
+
|
|
2462
|
+
const resizeHandle = ui.minimized
|
|
2463
|
+
? null
|
|
2464
|
+
: h('div', {
|
|
2465
|
+
className: 'dsh-np-resize',
|
|
2466
|
+
role: 'separator',
|
|
2467
|
+
title: t('resize'),
|
|
2468
|
+
'aria-label': t('resize'),
|
|
2469
|
+
onPointerDown: onResizePointerDown,
|
|
2470
|
+
});
|
|
2471
|
+
|
|
2472
|
+
const card = h(
|
|
2473
|
+
'section',
|
|
2474
|
+
{
|
|
2475
|
+
key: 'card',
|
|
2476
|
+
className: 'dsh-np-card',
|
|
2477
|
+
'data-dsh-notepad': 'true',
|
|
2478
|
+
'data-minimized': ui.minimized ? 'true' : 'false',
|
|
2479
|
+
'data-hidden': ui.hidden ? 'true' : 'false',
|
|
2480
|
+
'data-resizing': resizing ? 'true' : 'false',
|
|
2481
|
+
'data-state': status,
|
|
2482
|
+
'aria-hidden': ui.hidden ? 'true' : undefined,
|
|
2483
|
+
style: {
|
|
2484
|
+
left: `${left}px`,
|
|
2485
|
+
top: `${top}px`,
|
|
2486
|
+
width: `${displayWidth}px`,
|
|
2487
|
+
height: `${displayHeight}px`,
|
|
2488
|
+
},
|
|
2489
|
+
role: 'complementary',
|
|
2490
|
+
'aria-label': t('title'),
|
|
2491
|
+
},
|
|
2492
|
+
header,
|
|
2493
|
+
body,
|
|
2494
|
+
resizeHandle,
|
|
2495
|
+
);
|
|
2496
|
+
|
|
2497
|
+
const restore = h(
|
|
2498
|
+
'button',
|
|
2499
|
+
{
|
|
2500
|
+
key: 'restore',
|
|
2501
|
+
type: 'button',
|
|
2502
|
+
className: 'dsh-np-restore',
|
|
2503
|
+
'data-visible': ui.hidden ? 'true' : 'false',
|
|
2504
|
+
'aria-hidden': ui.hidden ? undefined : 'true',
|
|
2505
|
+
tabIndex: ui.hidden ? undefined : -1,
|
|
2506
|
+
title: t('show'),
|
|
2507
|
+
'aria-label': t('show'),
|
|
2508
|
+
onClick: toggleHidden,
|
|
2509
|
+
},
|
|
2510
|
+
h(NotepadIcon, { size: 16 }),
|
|
2511
|
+
);
|
|
2512
|
+
|
|
2513
|
+
return [card, restore];
|
|
882
2514
|
}
|
|
883
2515
|
|
|
884
2516
|
// ------------------------------------------------------------------
|