@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/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`; only the widget's position and
10
- * minimized/hidden state are kept in localStorage.
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 CARD_W = 372;
37
- const CARD_H = 336;
38
- const HEAD_H = 42;
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
- minimize: '最小化',
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
- hide: '隐藏(Ctrl+Shift+N)',
131
+ minimize: '最小化',
66
132
  show: '显示记事本(Ctrl+Shift+N)',
67
133
  chars: '{n} 字符',
68
- hint: 'Ctrl+Enter 插入 · Ctrl+Shift+N 隐藏',
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: 'Notepad content',
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
- inserted: 'Inserted',
84
- insertEmpty: 'The notepad is empty',
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
- minimize: 'Minimize',
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
- hide: 'Hide (Ctrl+Shift+N)',
216
+ minimize: 'Minimize',
92
217
  show: 'Show notepad (Ctrl+Shift+N)',
93
218
  chars: '{n} chars',
94
- hint: 'Ctrl+Enter inserts · Ctrl+Shift+N hides',
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: 'M8 2.2v6.6M5.6 6.6 8 9l2.4-2.4',
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: 'M3.1 10.2v1.3c0 .9.7 1.6 1.6 1.6h6.6c.9 0 1.6-.7 1.6-1.6v-1.3',
164
- stroke: 'currentColor', strokeWidth: 1.35, strokeLinecap: 'round',
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 MinusIcon({ size = 16 }) {
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: 'M3.8 8h8.4',
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 ChevronUpIcon({ size = 16 }) {
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: 'M4.2 9.8 8 6l3.8 3.8',
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
- function EyeOffIcon({ size = 16 }) {
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: 'M2.4 8s2.1-3.4 5.6-3.4S13.6 8 13.6 8s-2.1 3.4-5.6 3.4S2.4 8 2.4 8Z',
213
- stroke: 'currentColor', strokeWidth: 1.25,
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: 'M3 3l10 10',
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 CSS = `.dsh-np-card{position:fixed;z-index:30;display:flex;flex-direction:column;box-sizing:border-box;width:${CARD_W}px;max-width:calc(100vw - 16px);height:${CARD_H}px;border:1px solid var(--dsw-alias-border-l2,rgb(15 23 42/.12));border-radius:14px;background:var(--dsw-alias-bg-overlay,#fff);color:var(--dsw-alias-label-primary,#1f2430);box-shadow: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-size:13px;line-height:1.5}
228
- .dsh-np-card[data-minimized=true]{height:${HEAD_H}px}
229
- .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(--dsw-alias-bg-overlay,#fff);background:linear-gradient(180deg,var(--dsw-alias-bg-overlay,#fff),color-mix(in srgb,var(--dsw-alias-bg-overlay,#fff) 88%,var(--dsw-alias-bg-base,#f7f8fb)))}
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:#f5a524}
234
- .dsh-np-dot[data-state=saving]{background:#f5a524;animation:dsh-np-pulse 1s ease-in-out infinite}
235
- .dsh-np-dot[data-state=error]{background:#e25555}
236
- .dsh-np-dot[data-state=saved]{background:#2fb36d}
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-accent=true]{background:var(--dsw-alias-brand-primary,#4d6bfe);color:#fff}
243
- .dsh-np-btn[data-accent=true]:hover{background:var(--dsw-alias-brand-primary-hover,#3d57e8);color:#fff}
244
- .dsh-np-btn:disabled{opacity:.4;cursor:default;background:var(--dsw-alias-interactive-bg-hover,rgb(38 49 72/.06));color:var(--dsw-alias-label-tertiary,#6b7280)}
245
- .dsh-np-body{display:flex;flex:1 1 auto;flex-direction:column;min-height:0;padding:10px 12px 0}
246
- .dsh-np-text{flex:1 1 auto;width:100%;min-height:0;padding:0;border:0;outline:0;resize:none;background:transparent;color:inherit;font:12.5px/1.65 ui-monospace,SFMono-Regular,Consolas,"Liberation Mono",monospace;tab-size:2}
247
- .dsh-np-text::placeholder{color:var(--dsw-alias-label-caption,#9aa3b2)}
248
- .dsh-np-text:disabled{opacity:.65}
249
- .dsh-np-foot{display:flex;align-items:center;gap:8px;flex:none;padding:6px 12px 9px;color:var(--dsw-alias-label-tertiary,#6b7280);font-size:10.5px;line-height:14px}
250
- .dsh-np-status{flex:none;max-width:46%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
251
- .dsh-np-status[data-state=error]{color:#d94a4a}
252
- .dsh-np-retry{flex:none;padding:0;border:0;background:none;color:var(--dsw-alias-link,#4d6bfe);font:inherit;cursor:pointer;text-decoration:underline}
253
- .dsh-np-count{flex:none}
254
- .dsh-np-hint{margin-left:auto;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;opacity:.85}
255
- .dsh-np-toast{position:absolute;left:50%;bottom:38px;transform:translateX(-50%);max-width:calc(100% - 32px);padding:5px 10px;border-radius:8px;background:var(--dsw-alias-bg-overlay,#fff);border:1px solid var(--dsw-alias-border-l2,rgb(15 23 42/.12));box-shadow:0 8px 24px rgb(15 23 42/.18);color:var(--dsw-alias-label-primary,#1f2430);font-size:11.5px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;animation:dsh-np-rise .16s ease-out}
256
- .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-overlay,#fff);color:var(--dsw-alias-label-secondary,#4b5563);box-shadow:0 8px 24px rgb(15 23 42/.16);cursor:pointer;opacity:.86;transition:opacity .12s,transform .12s}
257
- .dsh-np-restore:hover{opacity:1;transform:translateY(-1px)}
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
- @media (prefers-reduced-motion:reduce){.dsh-np-toast,.dsh-np-restore{animation:none;transition:none}.dsh-np-dot[data-state=saving]{animation:none}}`;
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 `![${label}](${target})`;
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 = { x: null, y: null, minimized: false, hidden: false };
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 width = Math.min(CARD_W, Math.max(200, vw - 16));
292
- const height = ui.minimized ? HEAD_H : CARD_H;
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 - height - 24
298
- : Math.min(Math.max(8, ui.y), Math.max(8, vh - height - 8));
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, text) {
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, text }),
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 [text, setText] = useState('');
394
- const [loaded, setLoaded] = useState(false);
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 textRef = useRef('');
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 = textRef.current;
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 (textRef.current === body) {
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
- setLoaded(false);
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
- setLoaded(false);
1216
+ loadFailedRef.current = false;
1217
+ segmentsRef.current = [];
1218
+ setSegments([]);
1219
+ setSelection(new Set());
551
1220
  setStatus('loading');
552
1221
  setError('');
553
- setText('');
554
- textRef.current = '';
555
- fetchNote(root, binding.id)
556
- .then((data) => {
557
- if (cancelled || rootRef.current !== root) return;
558
- const value = typeof data.text === 'string' ? data.text : '';
559
- textRef.current = value;
560
- setText(value);
561
- versionRef.current = typeof data.mtimeMs === 'number' ? data.mtimeMs : null;
562
- setSavedAt(typeof data.mtimeMs === 'number' ? data.mtimeMs : null);
563
- dirtyRef.current = false;
564
- setLoaded(true);
565
- setStatus('saved');
566
- })
567
- .catch((reason) => {
568
- if (cancelled || rootRef.current !== root) return;
569
- setLoaded(true);
570
- setStatus('error');
571
- setError(String(reason?.message || reason));
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
- if (typeof document !== 'undefined' && document.activeElement === taRef.current) return;
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
- const value = typeof data.text === 'string' ? data.text : '';
629
- textRef.current = value;
630
- setText(value);
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
- // Actions -------------------------------------------------------------
653
- const onChange = (event) => {
654
- const value = event.target.value;
655
- textRef.current = value;
656
- setText(value);
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
- const insertIntoComposer = async () => {
664
- const value = textRef.current;
665
- if (value.trim() === '') {
666
- showToast(t('insertEmpty'));
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 actions = inputActionsRef.current;
670
- if (
671
- actions !== null &&
672
- actions !== undefined &&
673
- typeof actions.captureInsertion === 'function' &&
674
- typeof actions.insertText === 'function'
675
- ) {
676
- try {
677
- const span = actions.captureInsertion();
678
- if (actions.insertText(value, span) === true) {
679
- actions.persistDraft?.();
680
- showToast(t('inserted'));
681
- return;
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
- } catch {
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 copyAll = async () => {
692
- const value = textRef.current;
693
- if (value === '') {
694
- showToast(t('insertEmpty'));
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
- const toggleMinimize = () => {
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 onTextKeyDown = (event) => {
709
- if ((event.ctrlKey || event.metaKey) && event.key === 'Enter') {
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
- insertIntoComposer();
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 width = Math.min(CARD_W, Math.max(200, vw - 16));
726
- const height = ui.minimized ? HEAD_H : CARD_H;
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
- if (ui.hidden === true) {
747
- return h(
748
- 'button',
749
- {
750
- type: 'button',
751
- className: 'dsh-np-restore',
752
- title: t('show'),
753
- 'aria-label': t('show'),
754
- onClick: () => setUi((previous) => clampUi({ ...previous, hidden: false })),
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
- return h(
776
- 'section',
777
- {
778
- className: 'dsh-np-card',
779
- 'data-dsh-notepad': 'true',
780
- 'data-minimized': ui.minimized ? 'true' : 'false',
781
- 'data-state': status,
782
- style: { left: `${left}px`, top: `${top}px` },
783
- role: 'complementary',
784
- 'aria-label': t('title'),
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-head', onPointerDown: onHeaderPointerDown },
789
- h('span', { className: 'dsh-np-dot', 'data-state': status }),
790
- h('span', { className: 'dsh-np-title' }, t('title')),
791
- h('span', { className: 'dsh-np-workspace', title: binding.cwd }, workspaceName(binding.cwd)),
792
- h('span', { className: 'dsh-np-spacer' }),
793
- h(
794
- 'button',
795
- {
796
- type: 'button',
797
- className: 'dsh-np-btn',
798
- 'data-accent': 'true',
799
- title: t('insert'),
800
- 'aria-label': t('insert'),
801
- disabled: text.trim() === '',
802
- onClick: insertIntoComposer,
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
- h(InsertIcon, { size: 15 }),
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
- 'button',
808
- {
809
- type: 'button',
810
- className: 'dsh-np-btn',
811
- title: t('copy'),
812
- 'aria-label': t('copy'),
813
- disabled: text === '',
814
- onClick: copyAll,
815
- },
816
- h(CopyIcon, { size: 15 }),
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
- 'button',
2241
+ 'div',
820
2242
  {
821
- type: 'button',
822
- className: 'dsh-np-btn',
823
- title: ui.minimized ? t('expand') : t('minimize'),
824
- 'aria-label': ui.minimized ? t('expand') : t('minimize'),
825
- onClick: toggleMinimize,
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
- h(ui.minimized ? ChevronUpIcon : MinusIcon, { size: 15 }),
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
- h(
830
- 'button',
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
- type: 'button',
833
- className: 'dsh-np-btn',
834
- title: t('hide'),
835
- 'aria-label': t('hide'),
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(EyeOffIcon, { size: 15 }),
839
- ),
840
- ),
841
- ui.minimized
842
- ? null
843
- : h(
2300
+ h(
844
2301
  'div',
845
- { className: 'dsh-np-body' },
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-foot' },
864
- h('span', {
865
- className: 'dsh-np-status',
866
- 'data-state': status,
867
- title: error === '' ? undefined : error,
868
- }, statusText),
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-retry',
873
- onClick: () => saveNowRef.current?.({ force: true }),
874
- }, t('retry'))
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
- toast === null ? null : h('div', { className: 'dsh-np-toast', role: 'status' }, toast.message),
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
  // ------------------------------------------------------------------