dsh-quicknote 0.0.0-stage → 0.1.1
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/AGENTS.md +28 -0
- package/CHANGELOG.md +28 -0
- package/LICENSE +21 -0
- package/PUBLISHING.md +11 -0
- package/README.md +68 -2
- package/README.zh.md +67 -0
- package/cordis.patch.yml +5 -0
- package/icon.svg +4 -0
- package/lib/client.js +1407 -0
- package/lib/index.js +351 -0
- package/llms.txt +32 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +76 -4
package/lib/client.js
ADDED
|
@@ -0,0 +1,1407 @@
|
|
|
1
|
+
window.__ModuleLoader__.load({
|
|
2
|
+
id: "dsh-quicknote",
|
|
3
|
+
factory: (require) => {
|
|
4
|
+
var module = { exports: {} };
|
|
5
|
+
var exports = module.exports;
|
|
6
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
7
|
+
let react = require("react");
|
|
8
|
+
let react_jsx_runtime = require("react/jsx-runtime");
|
|
9
|
+
// src/shared/constants.ts
|
|
10
|
+
/** Package name; must equal package.json `name` (the client bundle id). */
|
|
11
|
+
const NAME = "dsh-quicknote";
|
|
12
|
+
/** Locale namespace owned by this plugin. */
|
|
13
|
+
const NS = "dsh-quicknote";
|
|
14
|
+
/** Host JSON API used by the page. */
|
|
15
|
+
const API = "/api/quicknote";
|
|
16
|
+
// src/client/session.ts
|
|
17
|
+
/** Access to the per-Session composer owned by the conversation service. */
|
|
18
|
+
let clientCtx = null;
|
|
19
|
+
function setClientContext(ctx) {
|
|
20
|
+
clientCtx = ctx;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Read an optional client service. Cordis throws on `ctx.<service>` for a
|
|
24
|
+
* service the plugin did not declare in `inject`, while `ctx.get(name)`
|
|
25
|
+
* returns `undefined` instead — so resolve through `get` first and keep the
|
|
26
|
+
* property path only as a fallback for older shells.
|
|
27
|
+
*/
|
|
28
|
+
function service(name) {
|
|
29
|
+
try {
|
|
30
|
+
const resolved = clientCtx?.get?.(name);
|
|
31
|
+
if (resolved !== void 0) return resolved;
|
|
32
|
+
} catch {}
|
|
33
|
+
try {
|
|
34
|
+
return clientCtx?.[name];
|
|
35
|
+
} catch {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
function sessionIdNear(node) {
|
|
40
|
+
try {
|
|
41
|
+
const id = (typeof node?.closest === "function" ? node.closest("[data-conversation-session]") : null)?.getAttribute("data-conversation-session");
|
|
42
|
+
return typeof id === "string" && id !== "" ? id : void 0;
|
|
43
|
+
} catch {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
function composerOf(sessionId) {
|
|
48
|
+
if (sessionId === void 0) return void 0;
|
|
49
|
+
try {
|
|
50
|
+
const input = service("conversation")?.input;
|
|
51
|
+
if (typeof input?.shell !== "function") return void 0;
|
|
52
|
+
const shell = input.shell(sessionId);
|
|
53
|
+
if (!shell?.state || !shell?.actions) return void 0;
|
|
54
|
+
return shell;
|
|
55
|
+
} catch {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
function draftOf(sessionId) {
|
|
60
|
+
try {
|
|
61
|
+
return String(composerOf(sessionId)?.state.getSnapshot().draft ?? "");
|
|
62
|
+
} catch {
|
|
63
|
+
return "";
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
/** The composer the user is most likely writing in. */
|
|
67
|
+
function focusedSessionId() {
|
|
68
|
+
try {
|
|
69
|
+
const fromFocus = sessionIdNear(document.activeElement);
|
|
70
|
+
if (fromFocus !== void 0) return fromFocus;
|
|
71
|
+
} catch {}
|
|
72
|
+
try {
|
|
73
|
+
return sessionIdNear(document.querySelector("[data-conversation-session]"));
|
|
74
|
+
} catch {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* The Session the main column points at, even while a global panel covers it.
|
|
80
|
+
* The shell unmounts the Conversation for a panel, so the DOM carries no
|
|
81
|
+
* `data-conversation-session`; the workspace selection and the retained
|
|
82
|
+
* main-view reference still name it.
|
|
83
|
+
*/
|
|
84
|
+
function mainSessionId() {
|
|
85
|
+
try {
|
|
86
|
+
const selected = service("uiWorkspace")?.selection?.getSnapshot?.()?.sessionId;
|
|
87
|
+
if (typeof selected === "string" && selected !== "") return selected;
|
|
88
|
+
} catch {}
|
|
89
|
+
try {
|
|
90
|
+
const byId = service("sessions")?.list?.getSnapshot?.()?.byId ?? {};
|
|
91
|
+
for (const row of Object.values(byId)) if ((row?.retainedBy?.mainView ?? 0) > 0 && typeof row?.id === "string" && row.id !== "") return row.id;
|
|
92
|
+
} catch {}
|
|
93
|
+
}
|
|
94
|
+
/** The composer to act on: the focused one, else the Session the main column retained. */
|
|
95
|
+
function targetSessionId() {
|
|
96
|
+
return focusedSessionId() ?? mainSessionId();
|
|
97
|
+
}
|
|
98
|
+
/** Insert a note into the active draft, preserving anything already typed. */
|
|
99
|
+
function insertIntoComposer(text) {
|
|
100
|
+
const sessionId = targetSessionId();
|
|
101
|
+
const composer = composerOf(sessionId);
|
|
102
|
+
if (!composer || !sessionId) return false;
|
|
103
|
+
try {
|
|
104
|
+
const current = draftOf(sessionId);
|
|
105
|
+
const next = current.trim() === "" ? text : `${current.replace(/\s+$/, "")}\n\n${text}`;
|
|
106
|
+
composer.actions.setDraft(next);
|
|
107
|
+
return true;
|
|
108
|
+
} catch {
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
/** Provenance for a note captured from a Session. */
|
|
113
|
+
function originOf(sessionId) {
|
|
114
|
+
if (!sessionId) return {
|
|
115
|
+
sessionId: "",
|
|
116
|
+
title: ""
|
|
117
|
+
};
|
|
118
|
+
try {
|
|
119
|
+
const row = service("sessions")?.list?.getSnapshot?.()?.byId?.[sessionId];
|
|
120
|
+
return {
|
|
121
|
+
sessionId,
|
|
122
|
+
title: typeof row?.title === "string" ? row.title : ""
|
|
123
|
+
};
|
|
124
|
+
} catch {
|
|
125
|
+
return {
|
|
126
|
+
sessionId,
|
|
127
|
+
title: ""
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
/** Show the notes page in the main column; false when the shell has no layout. */
|
|
132
|
+
function openPanel() {
|
|
133
|
+
try {
|
|
134
|
+
const layout = service("layout");
|
|
135
|
+
if (typeof layout?.selectPanel !== "function") return false;
|
|
136
|
+
layout.selectPanel(NAME);
|
|
137
|
+
return true;
|
|
138
|
+
} catch {
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
/** Return the main column to the Conversation; false when the shell has no layout. */
|
|
143
|
+
function showConversation() {
|
|
144
|
+
try {
|
|
145
|
+
const layout = service("layout");
|
|
146
|
+
if (typeof layout?.selectPanel !== "function") return false;
|
|
147
|
+
layout.selectPanel(null);
|
|
148
|
+
return true;
|
|
149
|
+
} catch {
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
/** Open the capture card for the active composer. */
|
|
154
|
+
function openCapture() {
|
|
155
|
+
const sessionId = focusedSessionId();
|
|
156
|
+
window.dispatchEvent(new CustomEvent("dsh-quicknote:open", { detail: {
|
|
157
|
+
sessionId,
|
|
158
|
+
text: draftOf(sessionId)
|
|
159
|
+
} }));
|
|
160
|
+
}
|
|
161
|
+
// src/client/ComposerButton.tsx
|
|
162
|
+
/** A labelled button in the composer toolbar. No icon-only destructive control. */
|
|
163
|
+
function ComposerButton({ t }) {
|
|
164
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
165
|
+
type: "button",
|
|
166
|
+
onClick: openCapture,
|
|
167
|
+
"data-testid": "quicknote-composer",
|
|
168
|
+
style: {
|
|
169
|
+
display: "inline-flex",
|
|
170
|
+
alignItems: "center",
|
|
171
|
+
gap: 6,
|
|
172
|
+
padding: "5px 9px",
|
|
173
|
+
borderRadius: 8,
|
|
174
|
+
border: "1px solid var(--dsw-alias-border-l1, rgba(127,127,127,.24))",
|
|
175
|
+
background: "transparent",
|
|
176
|
+
color: "var(--dsw-alias-label-secondary, inherit)",
|
|
177
|
+
font: "inherit",
|
|
178
|
+
cursor: "pointer",
|
|
179
|
+
whiteSpace: "nowrap"
|
|
180
|
+
},
|
|
181
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
|
|
182
|
+
viewBox: "0 0 24 24",
|
|
183
|
+
width: "16",
|
|
184
|
+
height: "16",
|
|
185
|
+
fill: "none",
|
|
186
|
+
"aria-hidden": "true",
|
|
187
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("rect", {
|
|
188
|
+
x: "4",
|
|
189
|
+
y: "3.5",
|
|
190
|
+
width: "16",
|
|
191
|
+
height: "17",
|
|
192
|
+
rx: "3",
|
|
193
|
+
stroke: "currentColor",
|
|
194
|
+
strokeWidth: "1.6"
|
|
195
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
|
|
196
|
+
d: "M8 8h8M8 12h8M8 16h5",
|
|
197
|
+
stroke: "currentColor",
|
|
198
|
+
strokeWidth: "1.6",
|
|
199
|
+
strokeLinecap: "round"
|
|
200
|
+
})]
|
|
201
|
+
}), t("toolbar.capture")]
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
// src/client/notes.ts
|
|
205
|
+
/**
|
|
206
|
+
* `#tag` must not continue a word, so `example.com#fragment` and `#` alone are not tags.
|
|
207
|
+
* Letters, marks, numbers, `_` and `-` are allowed, which covers `#工作` and `#release-1`.
|
|
208
|
+
*/
|
|
209
|
+
const TAG_PATTERN = /(?<![\p{L}\p{M}\p{N}_/.#&-])#([\p{L}\p{M}\p{N}_-]{1,32})/gu;
|
|
210
|
+
/** Tags in first-seen order, de-duplicated case-insensitively. */
|
|
211
|
+
function extractTags(text) {
|
|
212
|
+
const tags = [];
|
|
213
|
+
const seen = /* @__PURE__ */ new Set();
|
|
214
|
+
for (const match of text.matchAll(TAG_PATTERN)) {
|
|
215
|
+
const tag = match[1];
|
|
216
|
+
const key = tag.toLowerCase();
|
|
217
|
+
if (seen.has(key)) continue;
|
|
218
|
+
seen.add(key);
|
|
219
|
+
tags.push(tag);
|
|
220
|
+
}
|
|
221
|
+
return tags;
|
|
222
|
+
}
|
|
223
|
+
/** Every tag in the list, most-used first; ties sort alphabetically. */
|
|
224
|
+
function collectTags(notes) {
|
|
225
|
+
const counts = /* @__PURE__ */ new Map();
|
|
226
|
+
for (const note of notes) for (const tag of extractTags(note.text)) {
|
|
227
|
+
const key = tag.toLowerCase();
|
|
228
|
+
const current = counts.get(key);
|
|
229
|
+
if (current) current.count += 1;
|
|
230
|
+
else counts.set(key, {
|
|
231
|
+
tag,
|
|
232
|
+
count: 1
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
return [...counts.values()].sort((left, right) => right.count - left.count || left.tag.localeCompare(right.tag));
|
|
236
|
+
}
|
|
237
|
+
/** Apply the done filter, free-text search and active tag together. */
|
|
238
|
+
function noteMatches(note, options) {
|
|
239
|
+
if (options.filter === "open" && note.done) return false;
|
|
240
|
+
if (options.filter === "done" && !note.done) return false;
|
|
241
|
+
const needle = options.query.trim().toLowerCase();
|
|
242
|
+
if (needle !== "" && !note.text.toLowerCase().includes(needle)) return false;
|
|
243
|
+
if (options.tag !== null) {
|
|
244
|
+
const wanted = options.tag.toLowerCase();
|
|
245
|
+
if (!extractTags(note.text).some((tag) => tag.toLowerCase() === wanted)) return false;
|
|
246
|
+
}
|
|
247
|
+
return true;
|
|
248
|
+
}
|
|
249
|
+
function filterNotes(notes, options) {
|
|
250
|
+
return notes.filter((note) => noteMatches(note, options));
|
|
251
|
+
}
|
|
252
|
+
function formatTime(value) {
|
|
253
|
+
const date = new Date(value);
|
|
254
|
+
if (Number.isNaN(date.getTime())) return "";
|
|
255
|
+
const now = /* @__PURE__ */ new Date();
|
|
256
|
+
const clock = `${String(date.getHours()).padStart(2, "0")}:${String(date.getMinutes()).padStart(2, "0")}`;
|
|
257
|
+
if (date.toDateString() === now.toDateString()) return clock;
|
|
258
|
+
const monthDay = `${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")}`;
|
|
259
|
+
return date.getFullYear() === now.getFullYear() ? `${monthDay} ${clock}` : `${date.getFullYear()}-${monthDay}`;
|
|
260
|
+
}
|
|
261
|
+
/** Markdown export keeps the text literal, so tags already survive the round trip. */
|
|
262
|
+
function notesToMarkdown(notes, title) {
|
|
263
|
+
if (notes.length === 0) return "";
|
|
264
|
+
const lines = [`# ${title}`, ""];
|
|
265
|
+
for (const note of notes) {
|
|
266
|
+
lines.push(`- [${note.done ? "x" : " "}] ${note.text.replace(/\n/g, "\n ")}`);
|
|
267
|
+
lines.push(` <sub>${formatTime(note.createdAt)}</sub>`);
|
|
268
|
+
lines.push("");
|
|
269
|
+
}
|
|
270
|
+
return lines.join("\n");
|
|
271
|
+
}
|
|
272
|
+
// src/client/capture.ts
|
|
273
|
+
/**
|
|
274
|
+
* Unsaved capture text parked while the card sends the user to the notes page.
|
|
275
|
+
*
|
|
276
|
+
* @module dsh-quicknote/client/capture
|
|
277
|
+
*/
|
|
278
|
+
let stashed = null;
|
|
279
|
+
/** Remember the text the user typed before leaving the card for the notes page. */
|
|
280
|
+
function stashCaptureText(sessionId, text) {
|
|
281
|
+
stashed = {
|
|
282
|
+
sessionId,
|
|
283
|
+
text
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* The text to show when the card opens again: the stashed edit for the same
|
|
288
|
+
* Session, otherwise the composer draft. Reading consumes the stash.
|
|
289
|
+
*/
|
|
290
|
+
function takeCaptureText(sessionId, draft) {
|
|
291
|
+
const entry = stashed;
|
|
292
|
+
stashed = null;
|
|
293
|
+
return entry !== null && entry.sessionId === sessionId ? entry.text : draft;
|
|
294
|
+
}
|
|
295
|
+
// src/client/parked.ts
|
|
296
|
+
let current = null;
|
|
297
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
298
|
+
function emit() {
|
|
299
|
+
for (const listener of listeners) listener();
|
|
300
|
+
}
|
|
301
|
+
function setParked(next) {
|
|
302
|
+
current = next;
|
|
303
|
+
emit();
|
|
304
|
+
}
|
|
305
|
+
function parkedDraft() {
|
|
306
|
+
return current;
|
|
307
|
+
}
|
|
308
|
+
function useParkedDraft() {
|
|
309
|
+
return (0, react.useSyncExternalStore)((listener) => {
|
|
310
|
+
listeners.add(listener);
|
|
311
|
+
return () => listeners.delete(listener);
|
|
312
|
+
}, parkedDraft, parkedDraft);
|
|
313
|
+
}
|
|
314
|
+
// src/client/Overlay.tsx
|
|
315
|
+
/** Capture card in the shell overlay; saves a note and optionally clears the draft. */
|
|
316
|
+
function Overlay({ t }) {
|
|
317
|
+
const [open, setOpen] = (0, react.useState)(false);
|
|
318
|
+
const [text, setText] = (0, react.useState)("");
|
|
319
|
+
const [sessionId, setSessionId] = (0, react.useState)(void 0);
|
|
320
|
+
const [draftText, setDraftText] = (0, react.useState)("");
|
|
321
|
+
const [error, setError] = (0, react.useState)("");
|
|
322
|
+
const [saving, setSaving] = (0, react.useState)(false);
|
|
323
|
+
const [total, setTotal] = (0, react.useState)(null);
|
|
324
|
+
(0, react.useEffect)(() => {
|
|
325
|
+
const onOpen = (event) => {
|
|
326
|
+
const detail = event.detail;
|
|
327
|
+
const draft = detail?.text ?? "";
|
|
328
|
+
setText(takeCaptureText(detail?.sessionId, draft));
|
|
329
|
+
setDraftText(draft);
|
|
330
|
+
setSessionId(detail?.sessionId);
|
|
331
|
+
setError("");
|
|
332
|
+
setTotal(null);
|
|
333
|
+
setOpen(true);
|
|
334
|
+
refreshTotal();
|
|
335
|
+
};
|
|
336
|
+
window.addEventListener("dsh-quicknote:open", onOpen);
|
|
337
|
+
return () => window.removeEventListener("dsh-quicknote:open", onOpen);
|
|
338
|
+
}, []);
|
|
339
|
+
/** The count is decoration for the link; a failed read just hides it. */
|
|
340
|
+
async function refreshTotal() {
|
|
341
|
+
try {
|
|
342
|
+
const response = await fetch(`${API}/notes`);
|
|
343
|
+
if (!response.ok) return;
|
|
344
|
+
const data = await response.json();
|
|
345
|
+
if (Array.isArray(data.notes)) setTotal(data.notes.length);
|
|
346
|
+
} catch {}
|
|
347
|
+
}
|
|
348
|
+
/** Leave the card for the notes page, keeping what the user typed for the next open. */
|
|
349
|
+
function viewAll() {
|
|
350
|
+
if (!openPanel()) {
|
|
351
|
+
setError(t("overlay.openFailed"));
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
stashCaptureText(sessionId, text);
|
|
355
|
+
setOpen(false);
|
|
356
|
+
}
|
|
357
|
+
(0, react.useEffect)(() => {
|
|
358
|
+
if (!open) return;
|
|
359
|
+
const onKey = (event) => {
|
|
360
|
+
if (event.key === "Escape") setOpen(false);
|
|
361
|
+
};
|
|
362
|
+
window.addEventListener("keydown", onKey);
|
|
363
|
+
return () => window.removeEventListener("keydown", onKey);
|
|
364
|
+
}, [open]);
|
|
365
|
+
async function save(clearDraft) {
|
|
366
|
+
if (text.trim() === "" || saving) return;
|
|
367
|
+
setSaving(true);
|
|
368
|
+
setError("");
|
|
369
|
+
try {
|
|
370
|
+
const response = await fetch(`${API}/notes/create`, {
|
|
371
|
+
method: "POST",
|
|
372
|
+
headers: { "content-type": "application/json" },
|
|
373
|
+
body: JSON.stringify({
|
|
374
|
+
text,
|
|
375
|
+
origin: originOf(sessionId)
|
|
376
|
+
})
|
|
377
|
+
});
|
|
378
|
+
const result = await response.json();
|
|
379
|
+
if (!response.ok || !result.ok) throw new Error(result.error ?? "save-failed");
|
|
380
|
+
if (clearDraft && sessionId !== void 0 && result.note?.id) {
|
|
381
|
+
setParked({
|
|
382
|
+
noteId: result.note.id,
|
|
383
|
+
text,
|
|
384
|
+
draftText,
|
|
385
|
+
sessionId
|
|
386
|
+
});
|
|
387
|
+
try {
|
|
388
|
+
composerOf(sessionId)?.actions.setDraft("");
|
|
389
|
+
} catch {}
|
|
390
|
+
}
|
|
391
|
+
setOpen(false);
|
|
392
|
+
window.dispatchEvent(new Event("dsh-quicknote:changed"));
|
|
393
|
+
} catch (cause) {
|
|
394
|
+
setError(String(cause.message));
|
|
395
|
+
} finally {
|
|
396
|
+
setSaving(false);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
if (!open) return null;
|
|
400
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
401
|
+
style: {
|
|
402
|
+
position: "fixed",
|
|
403
|
+
inset: 0,
|
|
404
|
+
zIndex: 50,
|
|
405
|
+
display: "flex",
|
|
406
|
+
alignItems: "center",
|
|
407
|
+
justifyContent: "center",
|
|
408
|
+
background: "rgba(15,17,21,.45)",
|
|
409
|
+
padding: 24
|
|
410
|
+
},
|
|
411
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
412
|
+
role: "dialog",
|
|
413
|
+
"aria-modal": "true",
|
|
414
|
+
"aria-label": t("overlay.title"),
|
|
415
|
+
style: {
|
|
416
|
+
width: "min(560px, 100%)",
|
|
417
|
+
padding: 20,
|
|
418
|
+
borderRadius: 14,
|
|
419
|
+
background: "var(--dsw-alias-bg-overlay, #fff)",
|
|
420
|
+
boxShadow: "var(--dsw-elevation-panel, 0 24px 70px rgba(0,0,0,.3))",
|
|
421
|
+
color: "var(--dsw-alias-label-primary, inherit)",
|
|
422
|
+
colorScheme: "light dark"
|
|
423
|
+
},
|
|
424
|
+
children: [
|
|
425
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
426
|
+
style: {
|
|
427
|
+
display: "flex",
|
|
428
|
+
alignItems: "baseline",
|
|
429
|
+
justifyContent: "space-between",
|
|
430
|
+
gap: 12
|
|
431
|
+
},
|
|
432
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
|
|
433
|
+
style: {
|
|
434
|
+
margin: 0,
|
|
435
|
+
fontSize: 18
|
|
436
|
+
},
|
|
437
|
+
children: t("overlay.title")
|
|
438
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
439
|
+
type: "button",
|
|
440
|
+
"data-testid": "quicknote-overlay-view-all",
|
|
441
|
+
onClick: viewAll,
|
|
442
|
+
style: {
|
|
443
|
+
padding: 0,
|
|
444
|
+
border: 0,
|
|
445
|
+
background: "none",
|
|
446
|
+
color: "var(--dsw-alias-brand-primary, #4d6bfe)",
|
|
447
|
+
font: "inherit",
|
|
448
|
+
fontSize: 13,
|
|
449
|
+
whiteSpace: "nowrap",
|
|
450
|
+
cursor: "pointer"
|
|
451
|
+
},
|
|
452
|
+
children: total === null ? t("overlay.viewAll") : t("overlay.viewAllCount").replace("{count}", String(total))
|
|
453
|
+
})]
|
|
454
|
+
}),
|
|
455
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
|
|
456
|
+
"data-testid": "quicknote-overlay-input",
|
|
457
|
+
autoFocus: true,
|
|
458
|
+
value: text,
|
|
459
|
+
onChange: (event) => setText(event.target.value),
|
|
460
|
+
rows: 5,
|
|
461
|
+
style: {
|
|
462
|
+
width: "100%",
|
|
463
|
+
boxSizing: "border-box",
|
|
464
|
+
marginTop: 12,
|
|
465
|
+
padding: 10,
|
|
466
|
+
borderRadius: 8,
|
|
467
|
+
border: "1px solid var(--dsw-alias-border-l1, rgba(127,127,127,.24))",
|
|
468
|
+
background: "var(--dsw-alias-bg-layer-1, transparent)",
|
|
469
|
+
color: "var(--dsw-alias-label-primary, inherit)",
|
|
470
|
+
font: "inherit",
|
|
471
|
+
resize: "vertical"
|
|
472
|
+
}
|
|
473
|
+
}),
|
|
474
|
+
error === "" ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
475
|
+
role: "alert",
|
|
476
|
+
style: {
|
|
477
|
+
color: "var(--dsw-alias-state-error-primary, #c0392b)",
|
|
478
|
+
margin: "8px 0 0"
|
|
479
|
+
},
|
|
480
|
+
children: error
|
|
481
|
+
}),
|
|
482
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
483
|
+
style: {
|
|
484
|
+
display: "flex",
|
|
485
|
+
justifyContent: "flex-end",
|
|
486
|
+
gap: 8,
|
|
487
|
+
marginTop: 14,
|
|
488
|
+
flexWrap: "wrap"
|
|
489
|
+
},
|
|
490
|
+
children: [
|
|
491
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
492
|
+
type: "button",
|
|
493
|
+
onClick: () => setOpen(false),
|
|
494
|
+
style: {
|
|
495
|
+
padding: "7px 12px",
|
|
496
|
+
borderRadius: 8,
|
|
497
|
+
border: "1px solid var(--dsw-alias-border-l1, rgba(127,127,127,.24))",
|
|
498
|
+
background: "var(--dsw-alias-bg-layer-1, transparent)",
|
|
499
|
+
color: "var(--dsw-alias-label-primary, inherit)",
|
|
500
|
+
font: "inherit",
|
|
501
|
+
cursor: "pointer"
|
|
502
|
+
},
|
|
503
|
+
children: t("overlay.cancel")
|
|
504
|
+
}),
|
|
505
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
506
|
+
type: "button",
|
|
507
|
+
disabled: saving,
|
|
508
|
+
onClick: () => void save(false),
|
|
509
|
+
style: {
|
|
510
|
+
padding: "7px 12px",
|
|
511
|
+
borderRadius: 8,
|
|
512
|
+
border: "1px solid var(--dsw-alias-border-l1, rgba(127,127,127,.24))",
|
|
513
|
+
background: "var(--dsw-alias-bg-layer-1, transparent)",
|
|
514
|
+
color: "var(--dsw-alias-label-primary, inherit)",
|
|
515
|
+
font: "inherit",
|
|
516
|
+
cursor: "pointer"
|
|
517
|
+
},
|
|
518
|
+
children: t("overlay.save")
|
|
519
|
+
}),
|
|
520
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
521
|
+
type: "button",
|
|
522
|
+
disabled: saving || sessionId === void 0,
|
|
523
|
+
onClick: () => void save(true),
|
|
524
|
+
style: {
|
|
525
|
+
padding: "7px 12px",
|
|
526
|
+
borderRadius: 8,
|
|
527
|
+
border: "1px solid var(--dsw-alias-border-l1, rgba(127,127,127,.24))",
|
|
528
|
+
background: "var(--dsw-alias-bg-layer-1, transparent)",
|
|
529
|
+
color: "var(--dsw-alias-label-primary, inherit)",
|
|
530
|
+
font: "inherit",
|
|
531
|
+
cursor: "pointer"
|
|
532
|
+
},
|
|
533
|
+
children: t("overlay.saveClear")
|
|
534
|
+
})
|
|
535
|
+
]
|
|
536
|
+
})
|
|
537
|
+
]
|
|
538
|
+
})
|
|
539
|
+
});
|
|
540
|
+
}
|
|
541
|
+
// src/client/ParkedBar.tsx
|
|
542
|
+
/** Receipt above the composer after a draft has been moved into Quick Note. */
|
|
543
|
+
function ParkedBar({ t, captureSessionId }) {
|
|
544
|
+
const parked = useParkedDraft();
|
|
545
|
+
const [busy, setBusy] = (0, react.useState)(false);
|
|
546
|
+
if (!parked) return null;
|
|
547
|
+
if (captureSessionId !== void 0 && parked.sessionId !== captureSessionId) return null;
|
|
548
|
+
const preview = parked.text.length > 72 ? `${parked.text.slice(0, 72)}…` : parked.text;
|
|
549
|
+
async function undo() {
|
|
550
|
+
if (!parked || busy) return;
|
|
551
|
+
setBusy(true);
|
|
552
|
+
try {
|
|
553
|
+
const response = await fetch(`${API}/notes/delete`, {
|
|
554
|
+
method: "POST",
|
|
555
|
+
headers: { "content-type": "application/json" },
|
|
556
|
+
body: JSON.stringify({ id: parked.noteId })
|
|
557
|
+
});
|
|
558
|
+
if (!response.ok) throw new Error(`undo-failed-${response.status}`);
|
|
559
|
+
const composer = composerOf(parked.sessionId);
|
|
560
|
+
if (composer) {
|
|
561
|
+
const current = draftOf(parked.sessionId);
|
|
562
|
+
const next = current.trim() === "" ? parked.draftText : `${current.replace(/\s+$/, "")}\n\n${parked.draftText}`;
|
|
563
|
+
composer.actions.setDraft(next);
|
|
564
|
+
}
|
|
565
|
+
setParked(null);
|
|
566
|
+
window.dispatchEvent(new Event("dsh-quicknote:changed"));
|
|
567
|
+
} catch {} finally {
|
|
568
|
+
setBusy(false);
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
572
|
+
role: "status",
|
|
573
|
+
style: {
|
|
574
|
+
display: "flex",
|
|
575
|
+
alignItems: "center",
|
|
576
|
+
gap: 8,
|
|
577
|
+
margin: "0 0 6px",
|
|
578
|
+
padding: "6px 10px",
|
|
579
|
+
borderRadius: 8,
|
|
580
|
+
borderLeft: "3px solid var(--dsw-alias-brand-primary, #4d6bfe)",
|
|
581
|
+
background: "var(--dsw-alias-bg-layer-2, rgba(127,127,127,.1))",
|
|
582
|
+
color: "var(--dsw-alias-label-secondary, inherit)",
|
|
583
|
+
fontSize: 12,
|
|
584
|
+
lineHeight: "18px"
|
|
585
|
+
},
|
|
586
|
+
children: [
|
|
587
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
588
|
+
style: {
|
|
589
|
+
flex: 1,
|
|
590
|
+
minWidth: 0,
|
|
591
|
+
overflow: "hidden",
|
|
592
|
+
textOverflow: "ellipsis",
|
|
593
|
+
whiteSpace: "nowrap"
|
|
594
|
+
},
|
|
595
|
+
children: [
|
|
596
|
+
t("parked.saved"),
|
|
597
|
+
" · ",
|
|
598
|
+
preview
|
|
599
|
+
]
|
|
600
|
+
}),
|
|
601
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
602
|
+
type: "button",
|
|
603
|
+
onClick: openPanel,
|
|
604
|
+
style: {
|
|
605
|
+
border: 0,
|
|
606
|
+
background: "none",
|
|
607
|
+
color: "var(--dsw-alias-brand-primary, #4d6bfe)",
|
|
608
|
+
font: "inherit",
|
|
609
|
+
cursor: "pointer",
|
|
610
|
+
whiteSpace: "nowrap"
|
|
611
|
+
},
|
|
612
|
+
children: t("parked.open")
|
|
613
|
+
}),
|
|
614
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
615
|
+
type: "button",
|
|
616
|
+
disabled: busy,
|
|
617
|
+
onClick: () => void undo(),
|
|
618
|
+
style: {
|
|
619
|
+
border: 0,
|
|
620
|
+
background: "none",
|
|
621
|
+
color: "var(--dsw-alias-label-secondary, inherit)",
|
|
622
|
+
font: "inherit",
|
|
623
|
+
cursor: busy ? "default" : "pointer",
|
|
624
|
+
whiteSpace: "nowrap",
|
|
625
|
+
opacity: busy ? .5 : 1
|
|
626
|
+
},
|
|
627
|
+
children: t("parked.undo")
|
|
628
|
+
}),
|
|
629
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
630
|
+
type: "button",
|
|
631
|
+
onClick: () => setParked(null),
|
|
632
|
+
"aria-label": t("parked.dismiss"),
|
|
633
|
+
style: {
|
|
634
|
+
border: 0,
|
|
635
|
+
background: "none",
|
|
636
|
+
color: "var(--dsw-alias-label-tertiary, inherit)",
|
|
637
|
+
font: "inherit",
|
|
638
|
+
fontSize: 14,
|
|
639
|
+
cursor: "pointer",
|
|
640
|
+
padding: "0 2px"
|
|
641
|
+
},
|
|
642
|
+
children: "×"
|
|
643
|
+
})
|
|
644
|
+
]
|
|
645
|
+
});
|
|
646
|
+
}
|
|
647
|
+
// src/client/styles.ts
|
|
648
|
+
/**
|
|
649
|
+
* Scoped page styles. Colours stay on DSH theme tokens so light and dark mode
|
|
650
|
+
* come from the shell; the media queries handle narrow main columns.
|
|
651
|
+
*
|
|
652
|
+
* @module dsh-quicknote/client/styles
|
|
653
|
+
*/
|
|
654
|
+
const PAGE_CSS = `
|
|
655
|
+
.qn-page {
|
|
656
|
+
box-sizing: border-box;
|
|
657
|
+
min-height: 100%;
|
|
658
|
+
max-width: 760px;
|
|
659
|
+
padding: 24px 28px 48px;
|
|
660
|
+
background: var(--dsw-alias-bg-base, transparent);
|
|
661
|
+
color: var(--dsw-alias-label-primary, inherit);
|
|
662
|
+
color-scheme: light dark;
|
|
663
|
+
}
|
|
664
|
+
.qn-back {
|
|
665
|
+
display: block;
|
|
666
|
+
max-width: 100%;
|
|
667
|
+
margin: 0 0 10px;
|
|
668
|
+
padding: 0;
|
|
669
|
+
border: 0;
|
|
670
|
+
background: none;
|
|
671
|
+
color: var(--dsw-alias-brand-primary, #4d6bfe);
|
|
672
|
+
font: inherit;
|
|
673
|
+
font-size: 13px;
|
|
674
|
+
text-align: left;
|
|
675
|
+
white-space: nowrap;
|
|
676
|
+
overflow: hidden;
|
|
677
|
+
text-overflow: ellipsis;
|
|
678
|
+
cursor: pointer;
|
|
679
|
+
}
|
|
680
|
+
.qn-back:hover { text-decoration: underline; }
|
|
681
|
+
.qn-back:focus-visible { outline: 2px solid var(--dsw-alias-brand-primary, #4d6bfe); outline-offset: 2px; }
|
|
682
|
+
.qn-title { margin: 0; font-size: 22px; line-height: 1.3; }
|
|
683
|
+
.qn-lede { margin: 6px 0 18px; color: var(--dsw-alias-label-secondary, inherit); }
|
|
684
|
+
.qn-compose { display: flex; gap: 8px; align-items: flex-start; }
|
|
685
|
+
.qn-textarea {
|
|
686
|
+
flex: 1;
|
|
687
|
+
min-width: 0;
|
|
688
|
+
resize: vertical;
|
|
689
|
+
padding: 10px;
|
|
690
|
+
border-radius: 8px;
|
|
691
|
+
border: 1px solid var(--dsw-alias-border-l1, rgba(127, 127, 127, 0.24));
|
|
692
|
+
background: var(--dsw-alias-bg-layer-1, transparent);
|
|
693
|
+
color: var(--dsw-alias-label-primary, inherit);
|
|
694
|
+
color-scheme: light dark;
|
|
695
|
+
font: inherit;
|
|
696
|
+
}
|
|
697
|
+
.qn-button {
|
|
698
|
+
padding: 5px 9px;
|
|
699
|
+
border-radius: 7px;
|
|
700
|
+
border: 1px solid var(--dsw-alias-border-l1, rgba(127, 127, 127, 0.24));
|
|
701
|
+
background: var(--dsw-alias-bg-layer-1, transparent);
|
|
702
|
+
color: var(--dsw-alias-label-primary, inherit);
|
|
703
|
+
font: inherit;
|
|
704
|
+
line-height: 1.35;
|
|
705
|
+
white-space: nowrap;
|
|
706
|
+
cursor: pointer;
|
|
707
|
+
}
|
|
708
|
+
.qn-button:hover:not(:disabled) { background: var(--dsw-alias-bg-layer-2, rgba(127, 127, 127, 0.12)); }
|
|
709
|
+
.qn-button:disabled { cursor: default; opacity: 0.5; }
|
|
710
|
+
.qn-button:focus-visible,
|
|
711
|
+
.qn-textarea:focus-visible,
|
|
712
|
+
.qn-search:focus-visible {
|
|
713
|
+
outline: 2px solid var(--dsw-alias-brand-primary, #4d6bfe);
|
|
714
|
+
outline-offset: 1px;
|
|
715
|
+
}
|
|
716
|
+
.qn-button[aria-pressed="true"] {
|
|
717
|
+
border-color: var(--dsw-alias-brand-primary, currentColor);
|
|
718
|
+
background: var(--dsw-alias-bg-layer-2, rgba(127, 127, 127, 0.12));
|
|
719
|
+
color: var(--dsw-alias-brand-primary, currentColor);
|
|
720
|
+
}
|
|
721
|
+
.qn-primary { padding: 9px 14px; border-radius: 8px; }
|
|
722
|
+
.qn-toolbar {
|
|
723
|
+
display: grid;
|
|
724
|
+
grid-template-columns: auto minmax(160px, 1fr) auto;
|
|
725
|
+
gap: 8px;
|
|
726
|
+
align-items: center;
|
|
727
|
+
margin-top: 14px;
|
|
728
|
+
}
|
|
729
|
+
.qn-filters { display: flex; gap: 6px; }
|
|
730
|
+
.qn-search {
|
|
731
|
+
min-width: 0;
|
|
732
|
+
padding: 5px 9px;
|
|
733
|
+
border-radius: 7px;
|
|
734
|
+
border: 1px solid var(--dsw-alias-border-l1, rgba(127, 127, 127, 0.24));
|
|
735
|
+
background: var(--dsw-alias-bg-layer-1, transparent);
|
|
736
|
+
color: var(--dsw-alias-label-primary, inherit);
|
|
737
|
+
font: inherit;
|
|
738
|
+
}
|
|
739
|
+
.qn-tags { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; margin-top: 10px; }
|
|
740
|
+
.qn-tags-label { font-size: 12px; color: var(--dsw-alias-label-tertiary, inherit); }
|
|
741
|
+
.qn-tag {
|
|
742
|
+
padding: 2px 9px;
|
|
743
|
+
border-radius: 999px;
|
|
744
|
+
font-size: 12px;
|
|
745
|
+
line-height: 1.5;
|
|
746
|
+
}
|
|
747
|
+
.qn-note-tags { display: flex; flex-wrap: wrap; gap: 4px; margin-top: 6px; }
|
|
748
|
+
.qn-note-tags .qn-tag { padding: 1px 7px; font-size: 11px; }
|
|
749
|
+
.qn-list { list-style: none; padding: 0; margin: 20px 0 0; display: grid; gap: 8px; }
|
|
750
|
+
.qn-note {
|
|
751
|
+
display: grid;
|
|
752
|
+
grid-template-columns: minmax(0, 1fr) auto;
|
|
753
|
+
gap: 12px;
|
|
754
|
+
align-items: start;
|
|
755
|
+
padding: 12px;
|
|
756
|
+
border: 1px solid var(--dsw-alias-border-l1, rgba(127, 127, 127, 0.24));
|
|
757
|
+
border-radius: 10px;
|
|
758
|
+
background: var(--dsw-alias-bg-layer-1, transparent);
|
|
759
|
+
}
|
|
760
|
+
.qn-note-body { min-width: 0; }
|
|
761
|
+
.qn-note-text {
|
|
762
|
+
margin: 0;
|
|
763
|
+
white-space: pre-wrap;
|
|
764
|
+
overflow-wrap: anywhere;
|
|
765
|
+
}
|
|
766
|
+
.qn-note-text-done { text-decoration: line-through; opacity: 0.6; }
|
|
767
|
+
.qn-note-meta { margin-top: 4px; font-size: 11px; color: var(--dsw-alias-label-tertiary, inherit); }
|
|
768
|
+
.qn-note-actions { display: flex; flex-wrap: wrap; gap: 4px; justify-content: flex-end; }
|
|
769
|
+
.qn-alert { color: var(--dsw-alias-state-error-primary, #c0392b); }
|
|
770
|
+
.qn-status { margin: 8px 0 0; color: var(--dsw-alias-label-secondary, inherit); }
|
|
771
|
+
.qn-empty { margin-top: 24px; color: var(--dsw-alias-label-secondary, inherit); }
|
|
772
|
+
.qn-toast {
|
|
773
|
+
position: fixed;
|
|
774
|
+
z-index: 60;
|
|
775
|
+
left: 50%;
|
|
776
|
+
bottom: 24px;
|
|
777
|
+
transform: translateX(-50%);
|
|
778
|
+
display: flex;
|
|
779
|
+
align-items: center;
|
|
780
|
+
gap: 12px;
|
|
781
|
+
max-width: min(560px, calc(100vw - 32px));
|
|
782
|
+
padding: 10px 12px;
|
|
783
|
+
border: 1px solid var(--dsw-alias-border-l1, rgba(127, 127, 127, 0.24));
|
|
784
|
+
border-radius: 10px;
|
|
785
|
+
background: var(--dsw-alias-bg-overlay, #fff);
|
|
786
|
+
box-shadow: var(--dsw-elevation-panel, 0 18px 50px rgba(0, 0, 0, 0.24));
|
|
787
|
+
color: var(--dsw-alias-label-primary, inherit);
|
|
788
|
+
color-scheme: light dark;
|
|
789
|
+
}
|
|
790
|
+
.qn-toast-text {
|
|
791
|
+
flex: 1;
|
|
792
|
+
min-width: 0;
|
|
793
|
+
overflow: hidden;
|
|
794
|
+
text-overflow: ellipsis;
|
|
795
|
+
white-space: nowrap;
|
|
796
|
+
font-size: 13px;
|
|
797
|
+
}
|
|
798
|
+
.qn-toast-actions { display: flex; flex: none; gap: 4px; }
|
|
799
|
+
@media (max-width: 720px) {
|
|
800
|
+
.qn-page { padding: 16px 14px 40px; }
|
|
801
|
+
.qn-compose { flex-direction: column; align-items: stretch; }
|
|
802
|
+
.qn-compose .qn-primary { width: 100%; }
|
|
803
|
+
.qn-toolbar { grid-template-columns: minmax(0, 1fr) auto; }
|
|
804
|
+
.qn-filters { grid-column: 1 / -1; overflow-x: auto; padding-bottom: 2px; }
|
|
805
|
+
.qn-note { grid-template-columns: minmax(0, 1fr); gap: 8px; }
|
|
806
|
+
.qn-note-actions { justify-content: flex-start; }
|
|
807
|
+
.qn-toast { left: 12px; right: 12px; bottom: 12px; transform: none; max-width: none; }
|
|
808
|
+
}
|
|
809
|
+
@media (max-width: 420px) {
|
|
810
|
+
.qn-tags { flex-wrap: nowrap; overflow-x: auto; padding-bottom: 2px; }
|
|
811
|
+
.qn-tags-label { flex: none; }
|
|
812
|
+
.qn-tag { flex: none; }
|
|
813
|
+
.qn-note-actions { gap: 6px; }
|
|
814
|
+
}
|
|
815
|
+
`;
|
|
816
|
+
// src/client/index.tsx
|
|
817
|
+
/**
|
|
818
|
+
* Browser half of the Quick Note example. React is provided by DSH at runtime.
|
|
819
|
+
*
|
|
820
|
+
* @module dsh-quicknote/client
|
|
821
|
+
*/
|
|
822
|
+
async function request(path, init) {
|
|
823
|
+
const response = await fetch(`${API}${path}`, {
|
|
824
|
+
headers: { "content-type": "application/json" },
|
|
825
|
+
...init
|
|
826
|
+
});
|
|
827
|
+
const data = await response.json();
|
|
828
|
+
if (!response.ok || !data.ok) throw new Error(data.error ?? `request-failed-${response.status}`);
|
|
829
|
+
return data;
|
|
830
|
+
}
|
|
831
|
+
function Icon({ size = 18 }) {
|
|
832
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
|
|
833
|
+
viewBox: "0 0 24 24",
|
|
834
|
+
width: size,
|
|
835
|
+
height: size,
|
|
836
|
+
fill: "none",
|
|
837
|
+
"aria-hidden": "true",
|
|
838
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("rect", {
|
|
839
|
+
x: "4",
|
|
840
|
+
y: "3.5",
|
|
841
|
+
width: "16",
|
|
842
|
+
height: "17",
|
|
843
|
+
rx: "3",
|
|
844
|
+
stroke: "currentColor",
|
|
845
|
+
strokeWidth: "1.6"
|
|
846
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
|
|
847
|
+
d: "M8 8h8M8 12h8M8 16h5",
|
|
848
|
+
stroke: "currentColor",
|
|
849
|
+
strokeWidth: "1.6",
|
|
850
|
+
strokeLinecap: "round"
|
|
851
|
+
})]
|
|
852
|
+
});
|
|
853
|
+
}
|
|
854
|
+
const UNDO_WINDOW = 8e3;
|
|
855
|
+
function previewOf(text) {
|
|
856
|
+
const flat = text.replace(/\s+/g, " ").trim();
|
|
857
|
+
return flat.length > 80 ? `${flat.slice(0, 80)}…` : flat;
|
|
858
|
+
}
|
|
859
|
+
function Page({ t }) {
|
|
860
|
+
const [notes, setNotes] = (0, react.useState)([]);
|
|
861
|
+
const [text, setText] = (0, react.useState)("");
|
|
862
|
+
const [error, setError] = (0, react.useState)("");
|
|
863
|
+
const [notice, setNotice] = (0, react.useState)("");
|
|
864
|
+
const [query, setQuery] = (0, react.useState)("");
|
|
865
|
+
const [filter, setFilter] = (0, react.useState)("all");
|
|
866
|
+
const [tag, setTag] = (0, react.useState)(null);
|
|
867
|
+
const [undo, setUndo] = (0, react.useState)([]);
|
|
868
|
+
async function load() {
|
|
869
|
+
try {
|
|
870
|
+
const data = await request("/notes");
|
|
871
|
+
setNotes(data.notes);
|
|
872
|
+
} catch (cause) {
|
|
873
|
+
setError(String(cause.message));
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
(0, react.useEffect)(() => {
|
|
877
|
+
load();
|
|
878
|
+
const changed = () => void load();
|
|
879
|
+
window.addEventListener("dsh-quicknote:changed", changed);
|
|
880
|
+
return () => window.removeEventListener("dsh-quicknote:changed", changed);
|
|
881
|
+
}, []);
|
|
882
|
+
(0, react.useEffect)(() => {
|
|
883
|
+
if (notice === "") return;
|
|
884
|
+
const timer = window.setTimeout(() => setNotice(""), 3e3);
|
|
885
|
+
return () => window.clearTimeout(timer);
|
|
886
|
+
}, [notice]);
|
|
887
|
+
(0, react.useEffect)(() => {
|
|
888
|
+
if (undo.length === 0) return;
|
|
889
|
+
const timer = window.setTimeout(() => setUndo([]), UNDO_WINDOW);
|
|
890
|
+
return () => window.clearTimeout(timer);
|
|
891
|
+
}, [undo]);
|
|
892
|
+
async function add(event) {
|
|
893
|
+
event.preventDefault();
|
|
894
|
+
if (text.trim() === "") return;
|
|
895
|
+
try {
|
|
896
|
+
await request("/notes/create", {
|
|
897
|
+
method: "POST",
|
|
898
|
+
body: JSON.stringify({
|
|
899
|
+
text,
|
|
900
|
+
origin: originOf(focusedSessionId())
|
|
901
|
+
})
|
|
902
|
+
});
|
|
903
|
+
setText("");
|
|
904
|
+
setError("");
|
|
905
|
+
await load();
|
|
906
|
+
} catch (cause) {
|
|
907
|
+
setError(String(cause.message));
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
async function toggle(note) {
|
|
911
|
+
try {
|
|
912
|
+
await request("/notes/update", {
|
|
913
|
+
method: "POST",
|
|
914
|
+
body: JSON.stringify({
|
|
915
|
+
id: note.id,
|
|
916
|
+
done: !note.done
|
|
917
|
+
})
|
|
918
|
+
});
|
|
919
|
+
await load();
|
|
920
|
+
} catch (cause) {
|
|
921
|
+
setError(String(cause.message));
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
async function remove(note) {
|
|
925
|
+
try {
|
|
926
|
+
const removed = (await request("/notes/delete", {
|
|
927
|
+
method: "POST",
|
|
928
|
+
body: JSON.stringify({ id: note.id })
|
|
929
|
+
})).note;
|
|
930
|
+
if (removed) setUndo((current) => [...current, removed]);
|
|
931
|
+
setError("");
|
|
932
|
+
await load();
|
|
933
|
+
} catch (cause) {
|
|
934
|
+
setError(String(cause.message));
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
async function undoDelete() {
|
|
938
|
+
if (undo.length === 0) return;
|
|
939
|
+
try {
|
|
940
|
+
await Promise.all(undo.map((note) => request("/notes/restore", {
|
|
941
|
+
method: "POST",
|
|
942
|
+
body: JSON.stringify({ note })
|
|
943
|
+
})));
|
|
944
|
+
setNotice(t(undo.length > 1 ? "panel.deletesUndone" : "panel.deleteUndone"));
|
|
945
|
+
setUndo([]);
|
|
946
|
+
setError("");
|
|
947
|
+
await load();
|
|
948
|
+
} catch (cause) {
|
|
949
|
+
setError(String(cause.message));
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
async function copy(note) {
|
|
953
|
+
try {
|
|
954
|
+
await navigator.clipboard.writeText(note.text);
|
|
955
|
+
setNotice(t("panel.copied"));
|
|
956
|
+
} catch {
|
|
957
|
+
setError(t("panel.copyFailed"));
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
function insert(note) {
|
|
961
|
+
setNotice(insertIntoComposer(note.text) ? t("panel.inserted") : t("panel.insertUnavailable"));
|
|
962
|
+
}
|
|
963
|
+
const tags = (0, react.useMemo)(() => collectTags(notes), [notes]);
|
|
964
|
+
(0, react.useEffect)(() => {
|
|
965
|
+
if (tag === null) return;
|
|
966
|
+
const wanted = tag.toLowerCase();
|
|
967
|
+
if (!tags.some((entry) => entry.tag.toLowerCase() === wanted)) setTag(null);
|
|
968
|
+
}, [tag, tags]);
|
|
969
|
+
const mainId = mainSessionId();
|
|
970
|
+
const mainTitle = mainId === void 0 ? "" : originOf(mainId).title;
|
|
971
|
+
const backHint = mainTitle === "" ? t("panel.back") : t("panel.backTo").replace("{title}", mainTitle);
|
|
972
|
+
const needle = query.trim().toLowerCase();
|
|
973
|
+
const counts = {
|
|
974
|
+
all: notes.length,
|
|
975
|
+
open: notes.filter((note) => !note.done).length,
|
|
976
|
+
done: notes.filter((note) => note.done).length
|
|
977
|
+
};
|
|
978
|
+
const visible = filterNotes(notes, {
|
|
979
|
+
filter,
|
|
980
|
+
query: needle,
|
|
981
|
+
tag
|
|
982
|
+
});
|
|
983
|
+
function activateTag(next) {
|
|
984
|
+
setTag((current) => current?.toLowerCase() === next.toLowerCase() ? null : next);
|
|
985
|
+
}
|
|
986
|
+
async function copyAll() {
|
|
987
|
+
try {
|
|
988
|
+
await navigator.clipboard.writeText(notesToMarkdown(visible, t("panel.title")));
|
|
989
|
+
setNotice(t("panel.copyAllDone"));
|
|
990
|
+
} catch {
|
|
991
|
+
setError(t("panel.copyFailed"));
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("style", {
|
|
995
|
+
"data-dsh-quicknote": "",
|
|
996
|
+
children: PAGE_CSS
|
|
997
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
998
|
+
className: "qn-page",
|
|
999
|
+
children: [
|
|
1000
|
+
mainId === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
1001
|
+
type: "button",
|
|
1002
|
+
className: "qn-back",
|
|
1003
|
+
"data-testid": "note-back",
|
|
1004
|
+
title: backHint,
|
|
1005
|
+
onClick: () => {
|
|
1006
|
+
if (!showConversation()) setError(t("panel.backFailed"));
|
|
1007
|
+
},
|
|
1008
|
+
children: ["← ", backHint]
|
|
1009
|
+
}),
|
|
1010
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h1", {
|
|
1011
|
+
className: "qn-title",
|
|
1012
|
+
children: t("panel.title")
|
|
1013
|
+
}),
|
|
1014
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1015
|
+
className: "qn-lede",
|
|
1016
|
+
children: t("panel.body")
|
|
1017
|
+
}),
|
|
1018
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("form", {
|
|
1019
|
+
className: "qn-compose",
|
|
1020
|
+
onSubmit: add,
|
|
1021
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
|
|
1022
|
+
"data-testid": "note-input",
|
|
1023
|
+
className: "qn-textarea",
|
|
1024
|
+
value: text,
|
|
1025
|
+
onChange: (event) => setText(event.target.value),
|
|
1026
|
+
placeholder: t("panel.placeholder"),
|
|
1027
|
+
rows: 3
|
|
1028
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1029
|
+
"data-testid": "note-add",
|
|
1030
|
+
className: "qn-button qn-primary",
|
|
1031
|
+
type: "submit",
|
|
1032
|
+
disabled: text.trim() === "",
|
|
1033
|
+
children: t("panel.add")
|
|
1034
|
+
})]
|
|
1035
|
+
}),
|
|
1036
|
+
notes.length === 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1037
|
+
className: "qn-toolbar",
|
|
1038
|
+
children: [
|
|
1039
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1040
|
+
className: "qn-filters",
|
|
1041
|
+
role: "group",
|
|
1042
|
+
"aria-label": t("panel.filters"),
|
|
1043
|
+
children: [
|
|
1044
|
+
["all", "panel.filterAll"],
|
|
1045
|
+
["open", "panel.filterOpen"],
|
|
1046
|
+
["done", "panel.filterDone"]
|
|
1047
|
+
].map(([id, label]) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
1048
|
+
type: "button",
|
|
1049
|
+
className: "qn-button",
|
|
1050
|
+
"aria-pressed": filter === id,
|
|
1051
|
+
onClick: () => setFilter(id),
|
|
1052
|
+
children: [
|
|
1053
|
+
t(label),
|
|
1054
|
+
" ",
|
|
1055
|
+
counts[id]
|
|
1056
|
+
]
|
|
1057
|
+
}, id))
|
|
1058
|
+
}),
|
|
1059
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
1060
|
+
type: "search",
|
|
1061
|
+
className: "qn-search",
|
|
1062
|
+
value: query,
|
|
1063
|
+
onChange: (event) => setQuery(event.target.value),
|
|
1064
|
+
placeholder: t("panel.search"),
|
|
1065
|
+
"aria-label": t("panel.search")
|
|
1066
|
+
}),
|
|
1067
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1068
|
+
type: "button",
|
|
1069
|
+
className: "qn-button",
|
|
1070
|
+
onClick: () => void copyAll(),
|
|
1071
|
+
disabled: visible.length === 0,
|
|
1072
|
+
children: t("panel.copyAll")
|
|
1073
|
+
})
|
|
1074
|
+
]
|
|
1075
|
+
}), tags.length === 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1076
|
+
className: "qn-tags",
|
|
1077
|
+
role: "group",
|
|
1078
|
+
"aria-label": t("panel.tags"),
|
|
1079
|
+
children: [
|
|
1080
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1081
|
+
className: "qn-tags-label",
|
|
1082
|
+
children: t("panel.tags")
|
|
1083
|
+
}),
|
|
1084
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1085
|
+
type: "button",
|
|
1086
|
+
className: "qn-button qn-tag",
|
|
1087
|
+
"aria-pressed": tag === null,
|
|
1088
|
+
onClick: () => setTag(null),
|
|
1089
|
+
children: t("panel.allTags")
|
|
1090
|
+
}),
|
|
1091
|
+
tags.map((entry) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
1092
|
+
type: "button",
|
|
1093
|
+
className: "qn-button qn-tag",
|
|
1094
|
+
"aria-pressed": tag?.toLowerCase() === entry.tag.toLowerCase(),
|
|
1095
|
+
onClick: () => activateTag(entry.tag),
|
|
1096
|
+
children: [
|
|
1097
|
+
"#",
|
|
1098
|
+
entry.tag,
|
|
1099
|
+
" ",
|
|
1100
|
+
entry.count
|
|
1101
|
+
]
|
|
1102
|
+
}, entry.tag.toLowerCase()))
|
|
1103
|
+
]
|
|
1104
|
+
})] }),
|
|
1105
|
+
error === "" ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1106
|
+
role: "alert",
|
|
1107
|
+
className: "qn-alert",
|
|
1108
|
+
children: error
|
|
1109
|
+
}),
|
|
1110
|
+
notice === "" ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1111
|
+
role: "status",
|
|
1112
|
+
className: "qn-status",
|
|
1113
|
+
children: notice
|
|
1114
|
+
}),
|
|
1115
|
+
notes.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1116
|
+
className: "qn-empty",
|
|
1117
|
+
children: t("panel.empty")
|
|
1118
|
+
}) : visible.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1119
|
+
className: "qn-empty",
|
|
1120
|
+
children: t("panel.noMatches")
|
|
1121
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
|
|
1122
|
+
className: "qn-list",
|
|
1123
|
+
children: visible.map((note) => {
|
|
1124
|
+
const noteTags = extractTags(note.text);
|
|
1125
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
|
|
1126
|
+
"data-testid": "note-item",
|
|
1127
|
+
className: "qn-note",
|
|
1128
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1129
|
+
className: "qn-note-body",
|
|
1130
|
+
children: [
|
|
1131
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1132
|
+
className: `qn-note-text${note.done ? " qn-note-text-done" : ""}`,
|
|
1133
|
+
children: note.text
|
|
1134
|
+
}),
|
|
1135
|
+
noteTags.length === 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1136
|
+
className: "qn-note-tags",
|
|
1137
|
+
children: noteTags.map((label) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
1138
|
+
type: "button",
|
|
1139
|
+
className: "qn-button qn-tag",
|
|
1140
|
+
"aria-pressed": tag?.toLowerCase() === label.toLowerCase(),
|
|
1141
|
+
onClick: () => activateTag(label),
|
|
1142
|
+
children: ["#", label]
|
|
1143
|
+
}, label.toLowerCase()))
|
|
1144
|
+
}),
|
|
1145
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1146
|
+
className: "qn-note-meta",
|
|
1147
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("time", {
|
|
1148
|
+
dateTime: new Date(note.createdAt).toISOString(),
|
|
1149
|
+
children: formatTime(note.createdAt)
|
|
1150
|
+
}), note.origin?.title ? ` · ${note.origin.title}` : ""]
|
|
1151
|
+
})
|
|
1152
|
+
]
|
|
1153
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1154
|
+
className: "qn-note-actions",
|
|
1155
|
+
children: [
|
|
1156
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1157
|
+
type: "button",
|
|
1158
|
+
className: "qn-button",
|
|
1159
|
+
onClick: () => void copy(note),
|
|
1160
|
+
children: t("panel.copy")
|
|
1161
|
+
}),
|
|
1162
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1163
|
+
type: "button",
|
|
1164
|
+
className: "qn-button",
|
|
1165
|
+
onClick: () => insert(note),
|
|
1166
|
+
children: t("panel.insert")
|
|
1167
|
+
}),
|
|
1168
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1169
|
+
type: "button",
|
|
1170
|
+
className: "qn-button",
|
|
1171
|
+
onClick: () => void toggle(note),
|
|
1172
|
+
children: note.done ? t("panel.reopen") : t("panel.done")
|
|
1173
|
+
}),
|
|
1174
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1175
|
+
type: "button",
|
|
1176
|
+
className: "qn-button",
|
|
1177
|
+
title: t("panel.deleteHint"),
|
|
1178
|
+
onClick: () => void remove(note),
|
|
1179
|
+
children: t("panel.delete")
|
|
1180
|
+
})
|
|
1181
|
+
]
|
|
1182
|
+
})]
|
|
1183
|
+
}, note.id);
|
|
1184
|
+
})
|
|
1185
|
+
}),
|
|
1186
|
+
undo.length === 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1187
|
+
className: "qn-toast",
|
|
1188
|
+
role: "status",
|
|
1189
|
+
"aria-live": "polite",
|
|
1190
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1191
|
+
className: "qn-toast-text",
|
|
1192
|
+
children: [
|
|
1193
|
+
undo.length === 1 ? t("panel.deleted") : t("panel.deletedCount").replace("{count}", String(undo.length)),
|
|
1194
|
+
" ",
|
|
1195
|
+
previewOf(undo[undo.length - 1].text)
|
|
1196
|
+
]
|
|
1197
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1198
|
+
className: "qn-toast-actions",
|
|
1199
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1200
|
+
type: "button",
|
|
1201
|
+
className: "qn-button",
|
|
1202
|
+
onClick: () => void undoDelete(),
|
|
1203
|
+
children: undo.length > 1 ? t("panel.undoAll") : t("panel.undo")
|
|
1204
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1205
|
+
type: "button",
|
|
1206
|
+
className: "qn-button",
|
|
1207
|
+
"aria-label": t("panel.dismiss"),
|
|
1208
|
+
onClick: () => setUndo([]),
|
|
1209
|
+
children: "×"
|
|
1210
|
+
})]
|
|
1211
|
+
})]
|
|
1212
|
+
})
|
|
1213
|
+
]
|
|
1214
|
+
})] });
|
|
1215
|
+
}
|
|
1216
|
+
const name = NAME;
|
|
1217
|
+
const inject = ["slots", "locale"];
|
|
1218
|
+
function apply(ctx) {
|
|
1219
|
+
setClientContext(ctx);
|
|
1220
|
+
ctx.effect(() => ctx.locale.register(NS, {
|
|
1221
|
+
en: {
|
|
1222
|
+
"panel.title": "Quick Note",
|
|
1223
|
+
"panel.back": "Back to conversation",
|
|
1224
|
+
"panel.backTo": "Back to {title}",
|
|
1225
|
+
"panel.backFailed": "Could not open the conversation. Pick a Session in the sidebar.",
|
|
1226
|
+
"panel.body": "Capture a note, mark it done, keep the list on one page.",
|
|
1227
|
+
"panel.placeholder": "Write a note… Use #tags to group it.",
|
|
1228
|
+
"panel.add": "Add note",
|
|
1229
|
+
"panel.empty": "No notes yet.",
|
|
1230
|
+
"panel.noMatches": "No matching notes.",
|
|
1231
|
+
"panel.search": "Search notes",
|
|
1232
|
+
"panel.filters": "Filter notes",
|
|
1233
|
+
"panel.tags": "Tags",
|
|
1234
|
+
"panel.allTags": "All tags",
|
|
1235
|
+
"panel.filterAll": "All",
|
|
1236
|
+
"panel.filterOpen": "Open",
|
|
1237
|
+
"panel.filterDone": "Done",
|
|
1238
|
+
"panel.copy": "Copy",
|
|
1239
|
+
"panel.copied": "Copied.",
|
|
1240
|
+
"panel.copyFailed": "Could not copy this note.",
|
|
1241
|
+
"panel.copyAll": "Copy all",
|
|
1242
|
+
"panel.copyAllDone": "Copied the visible notes as Markdown.",
|
|
1243
|
+
"panel.insert": "Insert",
|
|
1244
|
+
"panel.inserted": "Inserted into the draft.",
|
|
1245
|
+
"panel.insertUnavailable": "No composer is open.",
|
|
1246
|
+
"panel.done": "Done",
|
|
1247
|
+
"panel.reopen": "Reopen",
|
|
1248
|
+
"panel.delete": "Delete",
|
|
1249
|
+
"panel.deleteHint": "Delete this note. You can undo it right after.",
|
|
1250
|
+
"panel.deleted": "Note deleted.",
|
|
1251
|
+
"panel.deletedCount": "Deleted {count} notes.",
|
|
1252
|
+
"panel.deleteUndone": "Note restored.",
|
|
1253
|
+
"panel.deletesUndone": "Notes restored.",
|
|
1254
|
+
"panel.undoAll": "Undo all",
|
|
1255
|
+
"panel.dismiss": "Dismiss",
|
|
1256
|
+
"panel.completed": "Note updated.",
|
|
1257
|
+
"panel.undo": "Undo",
|
|
1258
|
+
"toolbar.capture": "Quick Note",
|
|
1259
|
+
"overlay.title": "Save a quick note",
|
|
1260
|
+
"overlay.viewAll": "View all",
|
|
1261
|
+
"overlay.viewAllCount": "View all ({count})",
|
|
1262
|
+
"overlay.openFailed": "Could not open the notes page. Use Quick Note in the sidebar.",
|
|
1263
|
+
"overlay.cancel": "Cancel",
|
|
1264
|
+
"overlay.save": "Save note",
|
|
1265
|
+
"overlay.saveClear": "Save and clear draft",
|
|
1266
|
+
"overlay.empty": "Nothing to save.",
|
|
1267
|
+
"parked.saved": "Moved into Quick Note",
|
|
1268
|
+
"parked.open": "Open the list",
|
|
1269
|
+
"parked.undo": "Undo",
|
|
1270
|
+
"parked.dismiss": "Dismiss",
|
|
1271
|
+
"shortcut.open": "Open Quick Note"
|
|
1272
|
+
},
|
|
1273
|
+
zh: {
|
|
1274
|
+
"panel.title": "随手记",
|
|
1275
|
+
"panel.back": "返回对话",
|
|
1276
|
+
"panel.backTo": "返回「{title}」",
|
|
1277
|
+
"panel.backFailed": "打不开对话,请从左侧栏选择会话。",
|
|
1278
|
+
"panel.body": "记下一条笔记,标记完成,在一页里管理。",
|
|
1279
|
+
"panel.placeholder": "写一条笔记… 用 #标签 归类。",
|
|
1280
|
+
"panel.add": "添加",
|
|
1281
|
+
"panel.empty": "还没有笔记。",
|
|
1282
|
+
"panel.noMatches": "没有匹配的笔记。",
|
|
1283
|
+
"panel.search": "搜索笔记",
|
|
1284
|
+
"panel.filters": "筛选笔记",
|
|
1285
|
+
"panel.tags": "标签",
|
|
1286
|
+
"panel.allTags": "全部标签",
|
|
1287
|
+
"panel.filterAll": "全部",
|
|
1288
|
+
"panel.filterOpen": "未完成",
|
|
1289
|
+
"panel.filterDone": "已完成",
|
|
1290
|
+
"panel.copy": "复制",
|
|
1291
|
+
"panel.copied": "已复制。",
|
|
1292
|
+
"panel.copyFailed": "复制失败。",
|
|
1293
|
+
"panel.copyAll": "复制全部",
|
|
1294
|
+
"panel.copyAllDone": "已复制当前可见的笔记为 Markdown。",
|
|
1295
|
+
"panel.insert": "插入",
|
|
1296
|
+
"panel.inserted": "已插入草稿。",
|
|
1297
|
+
"panel.insertUnavailable": "当前没有可用的输入框。",
|
|
1298
|
+
"panel.done": "完成",
|
|
1299
|
+
"panel.reopen": "重新打开",
|
|
1300
|
+
"panel.delete": "删除",
|
|
1301
|
+
"panel.deleteHint": "删除这条笔记,随后可以撤销。",
|
|
1302
|
+
"panel.deleted": "笔记已删除。",
|
|
1303
|
+
"panel.deletedCount": "已删除 {count} 条笔记。",
|
|
1304
|
+
"panel.deleteUndone": "笔记已恢复。",
|
|
1305
|
+
"panel.deletesUndone": "笔记已恢复。",
|
|
1306
|
+
"panel.undoAll": "全部撤销",
|
|
1307
|
+
"panel.dismiss": "关闭提示",
|
|
1308
|
+
"panel.completed": "笔记已更新。",
|
|
1309
|
+
"panel.undo": "撤销",
|
|
1310
|
+
"toolbar.capture": "随手记",
|
|
1311
|
+
"overlay.title": "保存随手记",
|
|
1312
|
+
"overlay.viewAll": "查看全部",
|
|
1313
|
+
"overlay.viewAllCount": "查看全部({count})",
|
|
1314
|
+
"overlay.openFailed": "打不开笔记页,请从左侧栏的「随手记」进入。",
|
|
1315
|
+
"overlay.cancel": "取消",
|
|
1316
|
+
"overlay.save": "保存笔记",
|
|
1317
|
+
"overlay.saveClear": "保存并清空输入框",
|
|
1318
|
+
"overlay.empty": "没有可保存的内容。",
|
|
1319
|
+
"parked.saved": "已收进随手记",
|
|
1320
|
+
"parked.open": "打开列表",
|
|
1321
|
+
"parked.undo": "撤销",
|
|
1322
|
+
"parked.dismiss": "关闭提示",
|
|
1323
|
+
"shortcut.open": "打开随手记"
|
|
1324
|
+
}
|
|
1325
|
+
}), `${NAME}: dictionaries`);
|
|
1326
|
+
ctx.slots.inject("sidebar.panellist", () => ctx.slots.register({
|
|
1327
|
+
name: "sidebar.panellist",
|
|
1328
|
+
id: NAME,
|
|
1329
|
+
order: 60,
|
|
1330
|
+
label: () => ctx.locale.bind(NS)("panel.title"),
|
|
1331
|
+
locale: NS
|
|
1332
|
+
}, Icon));
|
|
1333
|
+
ctx.slots.inject("main", () => ctx.slots.register({
|
|
1334
|
+
name: "main",
|
|
1335
|
+
key: NAME,
|
|
1336
|
+
locale: NS
|
|
1337
|
+
}, Page));
|
|
1338
|
+
ctx.slots.inject("conversation.input.left", () => ctx.slots.register({
|
|
1339
|
+
name: "conversation.input.left",
|
|
1340
|
+
id: "quicknote.toolbar",
|
|
1341
|
+
order: 10,
|
|
1342
|
+
locale: NS
|
|
1343
|
+
}, ComposerButton));
|
|
1344
|
+
ctx.slots.inject("conversation.input.dock", () => ctx.slots.register({
|
|
1345
|
+
name: "conversation.input.dock",
|
|
1346
|
+
id: "quicknote.parked",
|
|
1347
|
+
order: 20,
|
|
1348
|
+
locale: NS,
|
|
1349
|
+
inject: (sessionId) => ({ captureSessionId: sessionId })
|
|
1350
|
+
}, ParkedBar));
|
|
1351
|
+
ctx.slots.inject("shell.overlay", () => ctx.slots.register({
|
|
1352
|
+
name: "shell.overlay",
|
|
1353
|
+
id: "quicknote.overlay",
|
|
1354
|
+
order: 40,
|
|
1355
|
+
locale: NS
|
|
1356
|
+
}, Overlay));
|
|
1357
|
+
if (typeof ctx.inject !== "function") return;
|
|
1358
|
+
ctx.inject(["shortcuts"], (scope) => {
|
|
1359
|
+
let shortcuts;
|
|
1360
|
+
try {
|
|
1361
|
+
shortcuts = scope.get("shortcuts");
|
|
1362
|
+
} catch {
|
|
1363
|
+
return;
|
|
1364
|
+
}
|
|
1365
|
+
if (typeof shortcuts?.register !== "function") return;
|
|
1366
|
+
scope.effect(() => {
|
|
1367
|
+
const binding = {
|
|
1368
|
+
code: "KeyS",
|
|
1369
|
+
modifiers: ["primary", "alt"]
|
|
1370
|
+
};
|
|
1371
|
+
return shortcuts.register({
|
|
1372
|
+
id: "quicknote.open",
|
|
1373
|
+
label: () => ctx.locale.bind(NS)("shortcut.open"),
|
|
1374
|
+
aliases: [
|
|
1375
|
+
"capture",
|
|
1376
|
+
"note",
|
|
1377
|
+
"quicknote",
|
|
1378
|
+
"随手记",
|
|
1379
|
+
"备忘"
|
|
1380
|
+
],
|
|
1381
|
+
defaults: {
|
|
1382
|
+
"desktop:macos": binding,
|
|
1383
|
+
"desktop:windows": binding,
|
|
1384
|
+
"desktop:linux": binding,
|
|
1385
|
+
"web:macos": binding,
|
|
1386
|
+
"web:windows": binding
|
|
1387
|
+
},
|
|
1388
|
+
regions: [
|
|
1389
|
+
"page",
|
|
1390
|
+
"editable",
|
|
1391
|
+
"terminal"
|
|
1392
|
+
],
|
|
1393
|
+
modals: [],
|
|
1394
|
+
resolve: () => ({
|
|
1395
|
+
status: "handled",
|
|
1396
|
+
run: openCapture
|
|
1397
|
+
})
|
|
1398
|
+
});
|
|
1399
|
+
}, "dsh-quicknote: shortcut");
|
|
1400
|
+
});
|
|
1401
|
+
}
|
|
1402
|
+
exports.apply = apply;
|
|
1403
|
+
exports.inject = inject;
|
|
1404
|
+
exports.name = name;
|
|
1405
|
+
return module.exports;
|
|
1406
|
+
}
|
|
1407
|
+
});
|