@restregular/dsh-notepad 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +84 -2
- package/client.js +947 -0
- package/cordis.patch.yml +9 -0
- package/icon.svg +19 -0
- package/index.js +266 -0
- package/lib/note-store.js +138 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +69 -4
package/client.js
ADDED
|
@@ -0,0 +1,947 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-notepad — Client half.
|
|
3
|
+
*
|
|
4
|
+
* Registers one floating notepad into `shell.overlay`. The panel is scoped to
|
|
5
|
+
* the workspace of the session currently retained by the main view, so every
|
|
6
|
+
* session opened from the same workspace shows the same note.
|
|
7
|
+
*
|
|
8
|
+
* 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.
|
|
11
|
+
*
|
|
12
|
+
* The whole file is wrapped in an IIFE: client bundles from every plugin are
|
|
13
|
+
* concatenated into the same classic-script realm, so top-level bindings would
|
|
14
|
+
* leak into a shared lexical scope and throw on reload.
|
|
15
|
+
*/
|
|
16
|
+
(() => {
|
|
17
|
+
const MODULE_ID = '@restregular/dsh-notepad';
|
|
18
|
+
|
|
19
|
+
/** Whether the host facade already holds a factory for this module id. */
|
|
20
|
+
function alreadyRegistered() {
|
|
21
|
+
const factories = window.__ModuleLoader__?.factories;
|
|
22
|
+
return factories !== undefined && typeof factories.has === 'function' && factories.has(MODULE_ID);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (!alreadyRegistered()) {
|
|
26
|
+
window.__ModuleLoader__.load({
|
|
27
|
+
id: MODULE_ID,
|
|
28
|
+
factory: (require) => {
|
|
29
|
+
const React = require('react');
|
|
30
|
+
const h = React.createElement;
|
|
31
|
+
const { useState, useEffect, useRef } = React;
|
|
32
|
+
|
|
33
|
+
const NS = 'dsh-notepad';
|
|
34
|
+
const API = '/dsh-notepad/api';
|
|
35
|
+
const UI_KEY = 'dsh-notepad.ui.v1';
|
|
36
|
+
const CARD_W = 372;
|
|
37
|
+
const CARD_H = 336;
|
|
38
|
+
const HEAD_H = 42;
|
|
39
|
+
const SAVE_DELAY = 500;
|
|
40
|
+
|
|
41
|
+
// ------------------------------------------------------------------
|
|
42
|
+
// Copy
|
|
43
|
+
// ------------------------------------------------------------------
|
|
44
|
+
const DICT = {
|
|
45
|
+
zh: {
|
|
46
|
+
title: '记事本',
|
|
47
|
+
editor: '记事本内容',
|
|
48
|
+
placeholder: '记录开发中遇到的问题,或下一阶段要做的事……',
|
|
49
|
+
loading: '读取中…',
|
|
50
|
+
empty: '还没有内容',
|
|
51
|
+
unsaved: '未保存',
|
|
52
|
+
saving: '保存中…',
|
|
53
|
+
saved: '已保存',
|
|
54
|
+
saveFailed: '保存失败',
|
|
55
|
+
retry: '重试',
|
|
56
|
+
insert: '插入到输入框',
|
|
57
|
+
inserted: '已插入输入框',
|
|
58
|
+
insertEmpty: '记事本还是空的',
|
|
59
|
+
insertUnavailable: '输入框暂不可用,内容已复制',
|
|
60
|
+
copy: '复制全部',
|
|
61
|
+
copied: '已复制',
|
|
62
|
+
copyFailed: '复制失败',
|
|
63
|
+
minimize: '最小化',
|
|
64
|
+
expand: '展开',
|
|
65
|
+
hide: '隐藏(Ctrl+Shift+N)',
|
|
66
|
+
show: '显示记事本(Ctrl+Shift+N)',
|
|
67
|
+
chars: '{n} 字符',
|
|
68
|
+
hint: 'Ctrl+Enter 插入 · Ctrl+Shift+N 隐藏',
|
|
69
|
+
workspace: '工作区',
|
|
70
|
+
},
|
|
71
|
+
en: {
|
|
72
|
+
title: 'Notepad',
|
|
73
|
+
editor: 'Notepad content',
|
|
74
|
+
placeholder: 'Jot down problems, or what to build next…',
|
|
75
|
+
loading: 'Loading…',
|
|
76
|
+
empty: 'Nothing here yet',
|
|
77
|
+
unsaved: 'Unsaved',
|
|
78
|
+
saving: 'Saving…',
|
|
79
|
+
saved: 'Saved',
|
|
80
|
+
saveFailed: 'Save failed',
|
|
81
|
+
retry: 'Retry',
|
|
82
|
+
insert: 'Insert into composer',
|
|
83
|
+
inserted: 'Inserted',
|
|
84
|
+
insertEmpty: 'The notepad is empty',
|
|
85
|
+
insertUnavailable: 'Composer unavailable; copied instead',
|
|
86
|
+
copy: 'Copy all',
|
|
87
|
+
copied: 'Copied',
|
|
88
|
+
copyFailed: 'Copy failed',
|
|
89
|
+
minimize: 'Minimize',
|
|
90
|
+
expand: 'Expand',
|
|
91
|
+
hide: 'Hide (Ctrl+Shift+N)',
|
|
92
|
+
show: 'Show notepad (Ctrl+Shift+N)',
|
|
93
|
+
chars: '{n} chars',
|
|
94
|
+
hint: 'Ctrl+Enter inserts · Ctrl+Shift+N hides',
|
|
95
|
+
workspace: 'Workspace',
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
function preferredLanguage() {
|
|
100
|
+
try {
|
|
101
|
+
const tag = String((typeof navigator !== 'undefined' && navigator.language) || 'zh').toLowerCase();
|
|
102
|
+
return tag.startsWith('zh') ? 'zh' : 'en';
|
|
103
|
+
} catch {
|
|
104
|
+
return 'zh';
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function fallbackTranslate() {
|
|
109
|
+
return (key, params) => {
|
|
110
|
+
const dict = DICT[preferredLanguage()] ?? DICT.zh;
|
|
111
|
+
let out = dict[key] ?? DICT.zh[key] ?? key;
|
|
112
|
+
if (params !== undefined && params !== null) {
|
|
113
|
+
out = out.replace(/\{(\w+)\}/gu, (match, name) =>
|
|
114
|
+
Object.prototype.hasOwnProperty.call(params, name) ? String(params[name]) : match);
|
|
115
|
+
}
|
|
116
|
+
return out;
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ------------------------------------------------------------------
|
|
121
|
+
// Icons
|
|
122
|
+
// ------------------------------------------------------------------
|
|
123
|
+
function Svg({ size = 16, children, viewBox = '0 0 16 16' }) {
|
|
124
|
+
return h(
|
|
125
|
+
'svg',
|
|
126
|
+
{
|
|
127
|
+
width: size,
|
|
128
|
+
height: size,
|
|
129
|
+
viewBox,
|
|
130
|
+
fill: 'none',
|
|
131
|
+
'aria-hidden': 'true',
|
|
132
|
+
focusable: 'false',
|
|
133
|
+
},
|
|
134
|
+
children,
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function NotepadIcon({ size = 16 }) {
|
|
139
|
+
return h(
|
|
140
|
+
Svg,
|
|
141
|
+
{ size, viewBox: '0 0 16 16' },
|
|
142
|
+
h('rect', {
|
|
143
|
+
x: 2.6, y: 1.9, width: 10.8, height: 12.2, rx: 2.2,
|
|
144
|
+
stroke: 'currentColor', strokeWidth: 1.25,
|
|
145
|
+
}),
|
|
146
|
+
h('path', {
|
|
147
|
+
d: 'M5.4 5.5h5.2M5.4 8h5.2M5.4 10.5h3',
|
|
148
|
+
stroke: 'currentColor', strokeWidth: 1.25, strokeLinecap: 'round',
|
|
149
|
+
}),
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function InsertIcon({ size = 16 }) {
|
|
154
|
+
return h(
|
|
155
|
+
Svg,
|
|
156
|
+
{ size, viewBox: '0 0 16 16' },
|
|
157
|
+
h('path', {
|
|
158
|
+
d: 'M8 2.2v6.6M5.6 6.6 8 9l2.4-2.4',
|
|
159
|
+
stroke: 'currentColor', strokeWidth: 1.35,
|
|
160
|
+
strokeLinecap: 'round', strokeLinejoin: 'round',
|
|
161
|
+
}),
|
|
162
|
+
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',
|
|
165
|
+
}),
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function CopyIcon({ size = 16 }) {
|
|
170
|
+
return h(
|
|
171
|
+
Svg,
|
|
172
|
+
{ size, viewBox: '0 0 16 16' },
|
|
173
|
+
h('rect', {
|
|
174
|
+
x: 5.2, y: 5.2, width: 8, height: 8, rx: 1.9,
|
|
175
|
+
stroke: 'currentColor', strokeWidth: 1.3,
|
|
176
|
+
}),
|
|
177
|
+
h('path', {
|
|
178
|
+
d: 'M10.8 3.4c0-.7-.6-1.3-1.3-1.3H4.2c-.7 0-1.3.6-1.3 1.3v5.3c0 .7.6 1.3 1.3 1.3',
|
|
179
|
+
stroke: 'currentColor', strokeWidth: 1.3, strokeLinecap: 'round',
|
|
180
|
+
}),
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function MinusIcon({ size = 16 }) {
|
|
185
|
+
return h(
|
|
186
|
+
Svg,
|
|
187
|
+
{ size, viewBox: '0 0 16 16' },
|
|
188
|
+
h('path', {
|
|
189
|
+
d: 'M3.8 8h8.4',
|
|
190
|
+
stroke: 'currentColor', strokeWidth: 1.4, strokeLinecap: 'round',
|
|
191
|
+
}),
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function ChevronUpIcon({ size = 16 }) {
|
|
196
|
+
return h(
|
|
197
|
+
Svg,
|
|
198
|
+
{ size, viewBox: '0 0 16 16' },
|
|
199
|
+
h('path', {
|
|
200
|
+
d: 'M4.2 9.8 8 6l3.8 3.8',
|
|
201
|
+
stroke: 'currentColor', strokeWidth: 1.4,
|
|
202
|
+
strokeLinecap: 'round', strokeLinejoin: 'round',
|
|
203
|
+
}),
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function EyeOffIcon({ size = 16 }) {
|
|
208
|
+
return h(
|
|
209
|
+
Svg,
|
|
210
|
+
{ size, viewBox: '0 0 16 16' },
|
|
211
|
+
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,
|
|
214
|
+
strokeLinecap: 'round', strokeLinejoin: 'round',
|
|
215
|
+
}),
|
|
216
|
+
h('circle', { cx: 8, cy: 8, r: 1.55, stroke: 'currentColor', strokeWidth: 1.25 }),
|
|
217
|
+
h('path', {
|
|
218
|
+
d: 'M3 3l10 10',
|
|
219
|
+
stroke: 'currentColor', strokeWidth: 1.35, strokeLinecap: 'round',
|
|
220
|
+
}),
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// ------------------------------------------------------------------
|
|
225
|
+
// Styles
|
|
226
|
+
// ------------------------------------------------------------------
|
|
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)))}
|
|
230
|
+
.dsh-np-card[data-minimized=true] .dsh-np-head{border-bottom-color:transparent}
|
|
231
|
+
.dsh-np-head:active{cursor:grabbing}
|
|
232
|
+
.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}
|
|
237
|
+
.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}
|
|
239
|
+
.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)}
|
|
258
|
+
@keyframes dsh-np-pulse{0%,100%{opacity:1}50%{opacity:.35}}
|
|
259
|
+
@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}}`;
|
|
261
|
+
|
|
262
|
+
// ------------------------------------------------------------------
|
|
263
|
+
// Helpers
|
|
264
|
+
// ------------------------------------------------------------------
|
|
265
|
+
function workspaceName(root) {
|
|
266
|
+
const parts = String(root ?? '').split(/[\\/]+/u).filter(Boolean);
|
|
267
|
+
return parts.length === 0 ? String(root ?? '') : parts[parts.length - 1];
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function readStoredUi() {
|
|
271
|
+
const base = { x: null, y: null, minimized: false, hidden: false };
|
|
272
|
+
try {
|
|
273
|
+
const raw = window.localStorage?.getItem(UI_KEY);
|
|
274
|
+
if (typeof raw !== 'string' || raw === '') return base;
|
|
275
|
+
const parsed = JSON.parse(raw);
|
|
276
|
+
if (parsed === null || typeof parsed !== 'object') return base;
|
|
277
|
+
return {
|
|
278
|
+
x: Number.isFinite(parsed.x) ? parsed.x : null,
|
|
279
|
+
y: Number.isFinite(parsed.y) ? parsed.y : null,
|
|
280
|
+
minimized: parsed.minimized === true,
|
|
281
|
+
hidden: parsed.hidden === true,
|
|
282
|
+
};
|
|
283
|
+
} catch {
|
|
284
|
+
return base;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function clampUi(ui) {
|
|
289
|
+
const vw = (typeof window !== 'undefined' && window.innerWidth) || 1280;
|
|
290
|
+
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;
|
|
293
|
+
const x = ui.x === null || ui.x === undefined
|
|
294
|
+
? vw - width - 24
|
|
295
|
+
: Math.min(Math.max(8, ui.x), Math.max(8, vw - width - 8));
|
|
296
|
+
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 };
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
async function readResponse(response) {
|
|
303
|
+
let data = {};
|
|
304
|
+
try {
|
|
305
|
+
data = await response.json();
|
|
306
|
+
} catch {
|
|
307
|
+
data = {};
|
|
308
|
+
}
|
|
309
|
+
if (!response.ok || data?.ok === false) {
|
|
310
|
+
throw new Error(data?.error || `HTTP ${response.status}`);
|
|
311
|
+
}
|
|
312
|
+
return data;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
async function fetchNote(root, sessionId) {
|
|
316
|
+
const query = new URLSearchParams({ root, sessionId });
|
|
317
|
+
const response = await fetch(`${API}/note?${query.toString()}`, {
|
|
318
|
+
method: 'GET',
|
|
319
|
+
cache: 'no-store',
|
|
320
|
+
});
|
|
321
|
+
return readResponse(response);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
async function saveNote(root, sessionId, text) {
|
|
325
|
+
const response = await fetch(`${API}/note`, {
|
|
326
|
+
method: 'PUT',
|
|
327
|
+
headers: { 'Content-Type': 'application/json' },
|
|
328
|
+
body: JSON.stringify({ root, sessionId, text }),
|
|
329
|
+
});
|
|
330
|
+
return readResponse(response);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
async function copyText(value) {
|
|
334
|
+
try {
|
|
335
|
+
if (typeof navigator !== 'undefined' && navigator.clipboard?.writeText) {
|
|
336
|
+
await navigator.clipboard.writeText(value);
|
|
337
|
+
return true;
|
|
338
|
+
}
|
|
339
|
+
} catch {
|
|
340
|
+
/* fall through to execCommand */
|
|
341
|
+
}
|
|
342
|
+
try {
|
|
343
|
+
const area = document.createElement('textarea');
|
|
344
|
+
area.value = value;
|
|
345
|
+
area.setAttribute('readonly', '');
|
|
346
|
+
area.style.position = 'fixed';
|
|
347
|
+
area.style.left = '-9999px';
|
|
348
|
+
area.style.top = '0';
|
|
349
|
+
document.body.appendChild(area);
|
|
350
|
+
area.select();
|
|
351
|
+
const ok = typeof document.execCommand === 'function' ? document.execCommand('copy') : false;
|
|
352
|
+
area.remove();
|
|
353
|
+
return ok;
|
|
354
|
+
} catch {
|
|
355
|
+
return false;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function clockLabel(value) {
|
|
360
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) return '';
|
|
361
|
+
try {
|
|
362
|
+
return new Date(value).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
|
363
|
+
} catch {
|
|
364
|
+
return '';
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function presentConversation() {
|
|
369
|
+
try {
|
|
370
|
+
if (typeof document === 'undefined') return false;
|
|
371
|
+
const node = document.querySelector('[data-conversation-content]');
|
|
372
|
+
if (node === null || node === undefined) return false;
|
|
373
|
+
if (typeof node.getBoundingClientRect === 'function') {
|
|
374
|
+
const rect = node.getBoundingClientRect();
|
|
375
|
+
return rect.width > 0 && rect.height > 0;
|
|
376
|
+
}
|
|
377
|
+
return true;
|
|
378
|
+
} catch {
|
|
379
|
+
return false;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// ------------------------------------------------------------------
|
|
384
|
+
// Overlay
|
|
385
|
+
// ------------------------------------------------------------------
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* @param {{ ctx: any, t: (key: string, params?: Record<string, unknown>) => string }} props
|
|
389
|
+
*/
|
|
390
|
+
function NotepadOverlay({ ctx, t }) {
|
|
391
|
+
const [binding, setBinding] = useState(null);
|
|
392
|
+
const [present, setPresent] = useState(presentConversation);
|
|
393
|
+
const [text, setText] = useState('');
|
|
394
|
+
const [loaded, setLoaded] = useState(false);
|
|
395
|
+
const [status, setStatus] = useState('idle');
|
|
396
|
+
const [error, setError] = useState('');
|
|
397
|
+
const [savedAt, setSavedAt] = useState(null);
|
|
398
|
+
const [toast, setToast] = useState(null);
|
|
399
|
+
const [ui, setUi] = useState(readStoredUi);
|
|
400
|
+
|
|
401
|
+
const bindingRef = useRef(null);
|
|
402
|
+
const rootSessionRef = useRef('');
|
|
403
|
+
const textRef = useRef('');
|
|
404
|
+
const rootRef = useRef('');
|
|
405
|
+
const versionRef = useRef(null);
|
|
406
|
+
const dirtyRef = useRef(false);
|
|
407
|
+
const timerRef = useRef(0);
|
|
408
|
+
const toastTimerRef = useRef(0);
|
|
409
|
+
const seqRef = useRef(0);
|
|
410
|
+
const saveNowRef = useRef(null);
|
|
411
|
+
const inputActionsRef = useRef(null);
|
|
412
|
+
const taRef = useRef(null);
|
|
413
|
+
const dragRef = useRef(null);
|
|
414
|
+
|
|
415
|
+
// Session + workspace -------------------------------------------------
|
|
416
|
+
useEffect(() => {
|
|
417
|
+
let disposed = false;
|
|
418
|
+
const uiSession = ctx.get('uiSession');
|
|
419
|
+
const sessions = ctx.get('sessions');
|
|
420
|
+
const current = uiSession?.adapter?.current;
|
|
421
|
+
if (current === undefined || sessions === undefined) {
|
|
422
|
+
setBinding(null);
|
|
423
|
+
return undefined;
|
|
424
|
+
}
|
|
425
|
+
const sync = () => {
|
|
426
|
+
if (disposed) return;
|
|
427
|
+
const snapshot = current.getSnapshot();
|
|
428
|
+
const id = typeof snapshot?.key === 'string' ? snapshot.key : undefined;
|
|
429
|
+
const row = id !== undefined ? sessions.list.getSnapshot()?.byId?.[id] : undefined;
|
|
430
|
+
const cwd = typeof row?.cwd === 'string' && row.cwd !== '' ? row.cwd : undefined;
|
|
431
|
+
inputActionsRef.current = snapshot?.props?.inputActions ?? null;
|
|
432
|
+
const next = id !== undefined && cwd !== undefined ? { id, cwd } : null;
|
|
433
|
+
bindingRef.current = next;
|
|
434
|
+
setBinding((previous) =>
|
|
435
|
+
previous !== null && next !== null && previous.id === next.id && previous.cwd === next.cwd
|
|
436
|
+
? previous
|
|
437
|
+
: next);
|
|
438
|
+
};
|
|
439
|
+
const unsubscribeCurrent = current.subscribe(sync);
|
|
440
|
+
const unsubscribeSessions = sessions.list.subscribe(sync);
|
|
441
|
+
sync();
|
|
442
|
+
return () => {
|
|
443
|
+
disposed = true;
|
|
444
|
+
unsubscribeCurrent?.();
|
|
445
|
+
unsubscribeSessions?.();
|
|
446
|
+
};
|
|
447
|
+
}, []);
|
|
448
|
+
|
|
449
|
+
// Only render over the conversation view, not over settings pages ----
|
|
450
|
+
useEffect(() => {
|
|
451
|
+
if (typeof document === 'undefined') return undefined;
|
|
452
|
+
const check = () => {
|
|
453
|
+
const visible = presentConversation();
|
|
454
|
+
setPresent((previous) => (previous === visible ? previous : visible));
|
|
455
|
+
};
|
|
456
|
+
check();
|
|
457
|
+
const timer = setInterval(check, 700);
|
|
458
|
+
return () => clearInterval(timer);
|
|
459
|
+
}, []);
|
|
460
|
+
|
|
461
|
+
// Position ------------------------------------------------------------
|
|
462
|
+
useEffect(() => {
|
|
463
|
+
setUi((previous) => clampUi(previous));
|
|
464
|
+
const onResize = () => setUi((previous) => clampUi(previous));
|
|
465
|
+
window.addEventListener('resize', onResize);
|
|
466
|
+
return () => window.removeEventListener('resize', onResize);
|
|
467
|
+
}, []);
|
|
468
|
+
|
|
469
|
+
useEffect(() => {
|
|
470
|
+
try {
|
|
471
|
+
window.localStorage?.setItem(UI_KEY, JSON.stringify(ui));
|
|
472
|
+
} catch {
|
|
473
|
+
/* Storage may be unavailable; the widget still works per page. */
|
|
474
|
+
}
|
|
475
|
+
}, [ui]);
|
|
476
|
+
|
|
477
|
+
// Save plumbing -------------------------------------------------------
|
|
478
|
+
const clearTimer = () => {
|
|
479
|
+
if (timerRef.current) {
|
|
480
|
+
clearTimeout(timerRef.current);
|
|
481
|
+
timerRef.current = 0;
|
|
482
|
+
}
|
|
483
|
+
};
|
|
484
|
+
|
|
485
|
+
const showToast = (message) => {
|
|
486
|
+
setToast({ id: Date.now(), message });
|
|
487
|
+
if (toastTimerRef.current) clearTimeout(toastTimerRef.current);
|
|
488
|
+
toastTimerRef.current = setTimeout(() => setToast(null), 1800);
|
|
489
|
+
};
|
|
490
|
+
|
|
491
|
+
const scheduleSave = () => {
|
|
492
|
+
clearTimer();
|
|
493
|
+
timerRef.current = setTimeout(() => {
|
|
494
|
+
timerRef.current = 0;
|
|
495
|
+
saveNowRef.current?.();
|
|
496
|
+
}, SAVE_DELAY);
|
|
497
|
+
};
|
|
498
|
+
|
|
499
|
+
const saveNow = async (options = {}) => {
|
|
500
|
+
clearTimer();
|
|
501
|
+
const root = rootRef.current;
|
|
502
|
+
const sessionId = rootSessionRef.current;
|
|
503
|
+
if (root === '' || sessionId === '') return;
|
|
504
|
+
if (!dirtyRef.current && options.force !== true) return;
|
|
505
|
+
const body = textRef.current;
|
|
506
|
+
const seq = ++seqRef.current;
|
|
507
|
+
dirtyRef.current = false;
|
|
508
|
+
setStatus('saving');
|
|
509
|
+
setError('');
|
|
510
|
+
try {
|
|
511
|
+
const data = await saveNote(root, sessionId, body);
|
|
512
|
+
if (seq !== seqRef.current || rootRef.current !== root) return;
|
|
513
|
+
if (typeof data.mtimeMs === 'number') versionRef.current = data.mtimeMs;
|
|
514
|
+
setSavedAt(typeof data.mtimeMs === 'number' ? data.mtimeMs : Date.now());
|
|
515
|
+
if (textRef.current === body) {
|
|
516
|
+
dirtyRef.current = false;
|
|
517
|
+
setStatus('saved');
|
|
518
|
+
} else {
|
|
519
|
+
dirtyRef.current = true;
|
|
520
|
+
setStatus('dirty');
|
|
521
|
+
scheduleSave();
|
|
522
|
+
}
|
|
523
|
+
} catch (reason) {
|
|
524
|
+
if (seq !== seqRef.current || rootRef.current !== root) return;
|
|
525
|
+
dirtyRef.current = true;
|
|
526
|
+
setStatus('error');
|
|
527
|
+
setError(String(reason?.message || reason));
|
|
528
|
+
}
|
|
529
|
+
};
|
|
530
|
+
saveNowRef.current = saveNow;
|
|
531
|
+
|
|
532
|
+
// Load on workspace/session change ------------------------------------
|
|
533
|
+
useEffect(() => {
|
|
534
|
+
if (binding === null) {
|
|
535
|
+
rootRef.current = '';
|
|
536
|
+
rootSessionRef.current = '';
|
|
537
|
+
setLoaded(false);
|
|
538
|
+
setStatus('idle');
|
|
539
|
+
setError('');
|
|
540
|
+
return undefined;
|
|
541
|
+
}
|
|
542
|
+
if (dirtyRef.current && rootRef.current !== '' && rootRef.current !== binding.cwd) {
|
|
543
|
+
saveNowRef.current?.({ force: true });
|
|
544
|
+
}
|
|
545
|
+
clearTimer();
|
|
546
|
+
const root = binding.cwd;
|
|
547
|
+
rootRef.current = root;
|
|
548
|
+
rootSessionRef.current = binding.id;
|
|
549
|
+
let cancelled = false;
|
|
550
|
+
setLoaded(false);
|
|
551
|
+
setStatus('loading');
|
|
552
|
+
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
|
+
});
|
|
573
|
+
return () => {
|
|
574
|
+
cancelled = true;
|
|
575
|
+
};
|
|
576
|
+
}, [binding === null ? '' : binding.id, binding === null ? '' : binding.cwd]);
|
|
577
|
+
|
|
578
|
+
// Keyboard ------------------------------------------------------------
|
|
579
|
+
useEffect(() => {
|
|
580
|
+
const onKeyDown = (event) => {
|
|
581
|
+
const chord = event.ctrlKey || event.metaKey;
|
|
582
|
+
if (!chord || !event.shiftKey) return;
|
|
583
|
+
if (event.key !== 'N' && event.key !== 'n') return;
|
|
584
|
+
event.preventDefault();
|
|
585
|
+
setUi((previous) => clampUi({ ...previous, hidden: !previous.hidden }));
|
|
586
|
+
};
|
|
587
|
+
window.addEventListener('keydown', onKeyDown, true);
|
|
588
|
+
return () => {
|
|
589
|
+
window.removeEventListener('keydown', onKeyDown, true);
|
|
590
|
+
clearTimer();
|
|
591
|
+
if (toastTimerRef.current) clearTimeout(toastTimerRef.current);
|
|
592
|
+
};
|
|
593
|
+
}, []);
|
|
594
|
+
|
|
595
|
+
// Flush on page hide / reload -----------------------------------------
|
|
596
|
+
useEffect(() => {
|
|
597
|
+
const flush = () => {
|
|
598
|
+
if (dirtyRef.current) saveNowRef.current?.({ force: true });
|
|
599
|
+
};
|
|
600
|
+
const onVisibility = () => {
|
|
601
|
+
if (typeof document !== 'undefined' && document.visibilityState === 'hidden') flush();
|
|
602
|
+
};
|
|
603
|
+
window.addEventListener('pagehide', flush);
|
|
604
|
+
if (typeof document !== 'undefined') {
|
|
605
|
+
document.addEventListener('visibilitychange', onVisibility);
|
|
606
|
+
}
|
|
607
|
+
return () => {
|
|
608
|
+
flush();
|
|
609
|
+
window.removeEventListener('pagehide', flush);
|
|
610
|
+
if (typeof document !== 'undefined') {
|
|
611
|
+
document.removeEventListener('visibilitychange', onVisibility);
|
|
612
|
+
}
|
|
613
|
+
};
|
|
614
|
+
}, []);
|
|
615
|
+
|
|
616
|
+
// Adopt external edits when the window regains focus ------------------
|
|
617
|
+
useEffect(() => {
|
|
618
|
+
if (typeof window === 'undefined') return undefined;
|
|
619
|
+
const refresh = () => {
|
|
620
|
+
const root = rootRef.current;
|
|
621
|
+
const sessionId = rootSessionRef.current;
|
|
622
|
+
if (root === '' || sessionId === '' || dirtyRef.current || timerRef.current) return;
|
|
623
|
+
if (typeof document !== 'undefined' && document.activeElement === taRef.current) return;
|
|
624
|
+
fetchNote(root, sessionId)
|
|
625
|
+
.then((data) => {
|
|
626
|
+
if (rootRef.current !== root || dirtyRef.current) return;
|
|
627
|
+
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);
|
|
631
|
+
versionRef.current = typeof data.mtimeMs === 'number' ? data.mtimeMs : null;
|
|
632
|
+
setSavedAt(typeof data.mtimeMs === 'number' ? data.mtimeMs : null);
|
|
633
|
+
setStatus('saved');
|
|
634
|
+
})
|
|
635
|
+
.catch(() => {});
|
|
636
|
+
};
|
|
637
|
+
const onVisibility = () => {
|
|
638
|
+
if (typeof document !== 'undefined' && document.visibilityState === 'visible') refresh();
|
|
639
|
+
};
|
|
640
|
+
window.addEventListener('focus', refresh);
|
|
641
|
+
if (typeof document !== 'undefined') {
|
|
642
|
+
document.addEventListener('visibilitychange', onVisibility);
|
|
643
|
+
}
|
|
644
|
+
return () => {
|
|
645
|
+
window.removeEventListener('focus', refresh);
|
|
646
|
+
if (typeof document !== 'undefined') {
|
|
647
|
+
document.removeEventListener('visibilitychange', onVisibility);
|
|
648
|
+
}
|
|
649
|
+
};
|
|
650
|
+
}, []);
|
|
651
|
+
|
|
652
|
+
// Actions -------------------------------------------------------------
|
|
653
|
+
const onChange = (event) => {
|
|
654
|
+
const value = event.target.value;
|
|
655
|
+
textRef.current = value;
|
|
656
|
+
setText(value);
|
|
657
|
+
dirtyRef.current = true;
|
|
658
|
+
setStatus('dirty');
|
|
659
|
+
setError('');
|
|
660
|
+
scheduleSave();
|
|
661
|
+
};
|
|
662
|
+
|
|
663
|
+
const insertIntoComposer = async () => {
|
|
664
|
+
const value = textRef.current;
|
|
665
|
+
if (value.trim() === '') {
|
|
666
|
+
showToast(t('insertEmpty'));
|
|
667
|
+
return;
|
|
668
|
+
}
|
|
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;
|
|
682
|
+
}
|
|
683
|
+
} catch {
|
|
684
|
+
/* fall through to copy */
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
const copied = await copyText(value);
|
|
688
|
+
showToast(copied ? t('insertUnavailable') : t('copyFailed'));
|
|
689
|
+
};
|
|
690
|
+
|
|
691
|
+
const copyAll = async () => {
|
|
692
|
+
const value = textRef.current;
|
|
693
|
+
if (value === '') {
|
|
694
|
+
showToast(t('insertEmpty'));
|
|
695
|
+
return;
|
|
696
|
+
}
|
|
697
|
+
showToast((await copyText(value)) ? t('copied') : t('copyFailed'));
|
|
698
|
+
};
|
|
699
|
+
|
|
700
|
+
const toggleMinimize = () => {
|
|
701
|
+
setUi((previous) => clampUi({ ...previous, minimized: !previous.minimized }));
|
|
702
|
+
};
|
|
703
|
+
|
|
704
|
+
const toggleHidden = () => {
|
|
705
|
+
setUi((previous) => clampUi({ ...previous, hidden: !previous.hidden }));
|
|
706
|
+
};
|
|
707
|
+
|
|
708
|
+
const onTextKeyDown = (event) => {
|
|
709
|
+
if ((event.ctrlKey || event.metaKey) && event.key === 'Enter') {
|
|
710
|
+
event.preventDefault();
|
|
711
|
+
insertIntoComposer();
|
|
712
|
+
}
|
|
713
|
+
};
|
|
714
|
+
|
|
715
|
+
const onHeaderPointerDown = (event) => {
|
|
716
|
+
if (event.button !== undefined && event.button !== 0) return;
|
|
717
|
+
const target = event.target;
|
|
718
|
+
if (target !== null && typeof target?.closest === 'function' && target.closest('button') !== null) return;
|
|
719
|
+
if (typeof ui.x !== 'number' || typeof ui.y !== 'number') return;
|
|
720
|
+
const start = { px: event.clientX, py: event.clientY, x: ui.x, y: ui.y };
|
|
721
|
+
dragRef.current = start;
|
|
722
|
+
const move = (moveEvent) => {
|
|
723
|
+
const vw = window.innerWidth || 1280;
|
|
724
|
+
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));
|
|
728
|
+
const y = Math.min(Math.max(8, start.y + moveEvent.clientY - start.py), Math.max(8, vh - height - 8));
|
|
729
|
+
setUi((previous) => ({ ...previous, x, y }));
|
|
730
|
+
};
|
|
731
|
+
const stop = () => {
|
|
732
|
+
dragRef.current = null;
|
|
733
|
+
window.removeEventListener('pointermove', move);
|
|
734
|
+
window.removeEventListener('pointerup', stop);
|
|
735
|
+
window.removeEventListener('pointercancel', stop);
|
|
736
|
+
};
|
|
737
|
+
window.addEventListener('pointermove', move);
|
|
738
|
+
window.addEventListener('pointerup', stop);
|
|
739
|
+
window.addEventListener('pointercancel', stop);
|
|
740
|
+
event.preventDefault?.();
|
|
741
|
+
};
|
|
742
|
+
|
|
743
|
+
// Render --------------------------------------------------------------
|
|
744
|
+
if (binding === null || present !== true) return null;
|
|
745
|
+
|
|
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);
|
|
762
|
+
const left = typeof ui.x === 'number' ? ui.x : fallbackX;
|
|
763
|
+
const top = typeof ui.y === 'number' ? ui.y : fallbackY;
|
|
764
|
+
|
|
765
|
+
let statusText = '';
|
|
766
|
+
if (status === 'loading') statusText = t('loading');
|
|
767
|
+
else if (status === 'saving') statusText = t('saving');
|
|
768
|
+
else if (status === 'dirty') statusText = t('unsaved');
|
|
769
|
+
else if (status === 'error') statusText = t('saveFailed');
|
|
770
|
+
else if (status === 'saved') {
|
|
771
|
+
const clock = clockLabel(savedAt);
|
|
772
|
+
statusText = clock === '' ? t('saved') : `${t('saved')} · ${clock}`;
|
|
773
|
+
} else statusText = t('empty');
|
|
774
|
+
|
|
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
|
+
},
|
|
786
|
+
h(
|
|
787
|
+
'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,
|
|
803
|
+
},
|
|
804
|
+
h(InsertIcon, { size: 15 }),
|
|
805
|
+
),
|
|
806
|
+
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 }),
|
|
817
|
+
),
|
|
818
|
+
h(
|
|
819
|
+
'button',
|
|
820
|
+
{
|
|
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,
|
|
826
|
+
},
|
|
827
|
+
h(ui.minimized ? ChevronUpIcon : MinusIcon, { size: 15 }),
|
|
828
|
+
),
|
|
829
|
+
h(
|
|
830
|
+
'button',
|
|
831
|
+
{
|
|
832
|
+
type: 'button',
|
|
833
|
+
className: 'dsh-np-btn',
|
|
834
|
+
title: t('hide'),
|
|
835
|
+
'aria-label': t('hide'),
|
|
836
|
+
onClick: toggleHidden,
|
|
837
|
+
},
|
|
838
|
+
h(EyeOffIcon, { size: 15 }),
|
|
839
|
+
),
|
|
840
|
+
),
|
|
841
|
+
ui.minimized
|
|
842
|
+
? null
|
|
843
|
+
: h(
|
|
844
|
+
'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
|
+
}),
|
|
861
|
+
h(
|
|
862
|
+
'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'
|
|
870
|
+
? h('button', {
|
|
871
|
+
type: 'button',
|
|
872
|
+
className: 'dsh-np-retry',
|
|
873
|
+
onClick: () => saveNowRef.current?.({ force: true }),
|
|
874
|
+
}, t('retry'))
|
|
875
|
+
: 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
|
+
),
|
|
879
|
+
toast === null ? null : h('div', { className: 'dsh-np-toast', role: 'status' }, toast.message),
|
|
880
|
+
),
|
|
881
|
+
);
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
// ------------------------------------------------------------------
|
|
885
|
+
// Plugin body
|
|
886
|
+
// ------------------------------------------------------------------
|
|
887
|
+
function apply(ctx) {
|
|
888
|
+
let translate = fallbackTranslate();
|
|
889
|
+
|
|
890
|
+
try {
|
|
891
|
+
const style = document.createElement('style');
|
|
892
|
+
style.setAttribute('data-dsh-notepad', '');
|
|
893
|
+
style.textContent = CSS;
|
|
894
|
+
document.head.appendChild(style);
|
|
895
|
+
ctx.effect(() => () => style.remove(), 'dsh-notepad: styles');
|
|
896
|
+
} catch (error) {
|
|
897
|
+
console.warn('[dsh-notepad] style injection failed:', error?.message ?? error);
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
ctx.effect(() => {
|
|
901
|
+
const locale = ctx.get('locale');
|
|
902
|
+
if (locale === undefined) return undefined;
|
|
903
|
+
let dispose;
|
|
904
|
+
try {
|
|
905
|
+
dispose = locale.register(NS, { zh: DICT.zh, en: DICT.en });
|
|
906
|
+
translate = locale.bind(NS);
|
|
907
|
+
} catch (error) {
|
|
908
|
+
console.warn('[dsh-notepad] locale registration failed:', error?.message ?? error);
|
|
909
|
+
return undefined;
|
|
910
|
+
}
|
|
911
|
+
return () => {
|
|
912
|
+
translate = fallbackTranslate();
|
|
913
|
+
try {
|
|
914
|
+
dispose?.();
|
|
915
|
+
} catch {
|
|
916
|
+
/* Removing a dictionary must never break unload. */
|
|
917
|
+
}
|
|
918
|
+
};
|
|
919
|
+
}, 'dsh-notepad: dictionaries');
|
|
920
|
+
|
|
921
|
+
const slots = ctx.get('slots');
|
|
922
|
+
if (slots === undefined || typeof slots.register !== 'function' || typeof slots.inject !== 'function') {
|
|
923
|
+
console.warn('[dsh-notepad] slots service missing; floating notepad disabled');
|
|
924
|
+
return;
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
try {
|
|
928
|
+
ctx.effect(
|
|
929
|
+
() => slots.inject('shell.overlay', () => slots.register(
|
|
930
|
+
{ name: 'shell.overlay', id: 'notepad', order: 70 },
|
|
931
|
+
() => h(NotepadOverlay, { ctx, t: (key, params) => translate(key, params) }),
|
|
932
|
+
)),
|
|
933
|
+
'dsh-notepad: overlay',
|
|
934
|
+
);
|
|
935
|
+
} catch (error) {
|
|
936
|
+
console.warn('[dsh-notepad] could not register shell.overlay:', error?.message ?? error);
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
return {
|
|
941
|
+
inject: ['slots'],
|
|
942
|
+
apply,
|
|
943
|
+
};
|
|
944
|
+
},
|
|
945
|
+
});
|
|
946
|
+
}
|
|
947
|
+
})();
|