@restregular/dsh-notepad 0.1.0 → 0.2.0

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