dsh-quicknote 0.1.0 → 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/lib/client.js CHANGED
@@ -19,6 +19,23 @@ window.__ModuleLoader__.load({
19
19
  function setClientContext(ctx) {
20
20
  clientCtx = ctx;
21
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
+ }
22
39
  function sessionIdNear(node) {
23
40
  try {
24
41
  const id = (typeof node?.closest === "function" ? node.closest("[data-conversation-session]") : null)?.getAttribute("data-conversation-session");
@@ -30,7 +47,7 @@ window.__ModuleLoader__.load({
30
47
  function composerOf(sessionId) {
31
48
  if (sessionId === void 0) return void 0;
32
49
  try {
33
- const input = clientCtx?.get("conversation")?.input;
50
+ const input = service("conversation")?.input;
34
51
  if (typeof input?.shell !== "function") return void 0;
35
52
  const shell = input.shell(sessionId);
36
53
  if (!shell?.state || !shell?.actions) return void 0;
@@ -46,19 +63,107 @@ window.__ModuleLoader__.load({
46
63
  return "";
47
64
  }
48
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
+ }
49
161
  // src/client/ComposerButton.tsx
50
162
  /** A labelled button in the composer toolbar. No icon-only destructive control. */
51
163
  function ComposerButton({ t }) {
52
- function open(event) {
53
- const sessionId = sessionIdNear(event.currentTarget);
54
- window.dispatchEvent(new CustomEvent("dsh-quicknote:open", { detail: {
55
- sessionId,
56
- text: draftOf(sessionId)
57
- } }));
58
- }
59
164
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
60
165
  type: "button",
61
- onClick: open,
166
+ onClick: openCapture,
62
167
  "data-testid": "quicknote-composer",
63
168
  style: {
64
169
  display: "inline-flex",
@@ -66,52 +171,189 @@ window.__ModuleLoader__.load({
66
171
  gap: 6,
67
172
  padding: "5px 9px",
68
173
  borderRadius: 8,
69
- border: "1px solid var(--dsw-alias-line, #d9dce1)",
174
+ border: "1px solid var(--dsw-alias-border-l1, rgba(127,127,127,.24))",
70
175
  background: "transparent",
71
- color: "inherit",
176
+ color: "var(--dsw-alias-label-secondary, inherit)",
72
177
  font: "inherit",
73
- cursor: "pointer"
178
+ cursor: "pointer",
179
+ whiteSpace: "nowrap"
74
180
  },
75
181
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
76
- viewBox: "0 0 16 16",
182
+ viewBox: "0 0 24 24",
77
183
  width: "16",
78
184
  height: "16",
79
185
  fill: "none",
80
186
  "aria-hidden": "true",
81
187
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("rect", {
82
- x: "2.75",
83
- y: "2.75",
84
- width: "10.5",
85
- height: "10.5",
86
- rx: "2",
87
- stroke: "currentColor"
188
+ x: "4",
189
+ y: "3.5",
190
+ width: "16",
191
+ height: "17",
192
+ rx: "3",
193
+ stroke: "currentColor",
194
+ strokeWidth: "1.6"
88
195
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
89
- d: "M5.5 8h5",
196
+ d: "M8 8h8M8 12h8M8 16h5",
90
197
  stroke: "currentColor",
198
+ strokeWidth: "1.6",
91
199
  strokeLinecap: "round"
92
200
  })]
93
201
  }), t("toolbar.capture")]
94
202
  });
95
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
+ }
96
314
  // src/client/Overlay.tsx
97
315
  /** Capture card in the shell overlay; saves a note and optionally clears the draft. */
98
316
  function Overlay({ t }) {
99
317
  const [open, setOpen] = (0, react.useState)(false);
100
318
  const [text, setText] = (0, react.useState)("");
101
319
  const [sessionId, setSessionId] = (0, react.useState)(void 0);
320
+ const [draftText, setDraftText] = (0, react.useState)("");
102
321
  const [error, setError] = (0, react.useState)("");
103
322
  const [saving, setSaving] = (0, react.useState)(false);
323
+ const [total, setTotal] = (0, react.useState)(null);
104
324
  (0, react.useEffect)(() => {
105
325
  const onOpen = (event) => {
106
326
  const detail = event.detail;
107
- setText(detail?.text ?? "");
327
+ const draft = detail?.text ?? "";
328
+ setText(takeCaptureText(detail?.sessionId, draft));
329
+ setDraftText(draft);
108
330
  setSessionId(detail?.sessionId);
109
331
  setError("");
332
+ setTotal(null);
110
333
  setOpen(true);
334
+ refreshTotal();
111
335
  };
112
336
  window.addEventListener("dsh-quicknote:open", onOpen);
113
337
  return () => window.removeEventListener("dsh-quicknote:open", onOpen);
114
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
+ }
115
357
  (0, react.useEffect)(() => {
116
358
  if (!open) return;
117
359
  const onKey = (event) => {
@@ -128,13 +370,24 @@ window.__ModuleLoader__.load({
128
370
  const response = await fetch(`${API}/notes/create`, {
129
371
  method: "POST",
130
372
  headers: { "content-type": "application/json" },
131
- body: JSON.stringify({ text })
373
+ body: JSON.stringify({
374
+ text,
375
+ origin: originOf(sessionId)
376
+ })
132
377
  });
133
378
  const result = await response.json();
134
379
  if (!response.ok || !result.ok) throw new Error(result.error ?? "save-failed");
135
- if (clearDraft && sessionId !== void 0) try {
136
- composerOf(sessionId)?.actions.setDraft("");
137
- } catch {}
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
+ }
138
391
  setOpen(false);
139
392
  window.dispatchEvent(new Event("dsh-quicknote:changed"));
140
393
  } catch (cause) {
@@ -163,16 +416,41 @@ window.__ModuleLoader__.load({
163
416
  width: "min(560px, 100%)",
164
417
  padding: 20,
165
418
  borderRadius: 14,
166
- background: "var(--dsw-alias-bg, #fff)",
167
- boxShadow: "0 24px 70px rgba(0,0,0,.3)"
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"
168
423
  },
169
424
  children: [
170
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
425
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
171
426
  style: {
172
- margin: 0,
173
- fontSize: 18
427
+ display: "flex",
428
+ alignItems: "baseline",
429
+ justifyContent: "space-between",
430
+ gap: 12
174
431
  },
175
- children: t("overlay.title")
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
+ })]
176
454
  }),
177
455
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
178
456
  "data-testid": "quicknote-overlay-input",
@@ -186,7 +464,9 @@ window.__ModuleLoader__.load({
186
464
  marginTop: 12,
187
465
  padding: 10,
188
466
  borderRadius: 8,
189
- border: "1px solid var(--dsw-alias-line, #d9dce1)",
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)",
190
470
  font: "inherit",
191
471
  resize: "vertical"
192
472
  }
@@ -194,7 +474,7 @@ window.__ModuleLoader__.load({
194
474
  error === "" ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
195
475
  role: "alert",
196
476
  style: {
197
- color: "var(--dsw-alias-danger, #c0392b)",
477
+ color: "var(--dsw-alias-state-error-primary, #c0392b)",
198
478
  margin: "8px 0 0"
199
479
  },
200
480
  children: error
@@ -213,7 +493,12 @@ window.__ModuleLoader__.load({
213
493
  onClick: () => setOpen(false),
214
494
  style: {
215
495
  padding: "7px 12px",
216
- borderRadius: 8
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"
217
502
  },
218
503
  children: t("overlay.cancel")
219
504
  }),
@@ -223,7 +508,12 @@ window.__ModuleLoader__.load({
223
508
  onClick: () => void save(false),
224
509
  style: {
225
510
  padding: "7px 12px",
226
- borderRadius: 8
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"
227
517
  },
228
518
  children: t("overlay.save")
229
519
  }),
@@ -233,7 +523,12 @@ window.__ModuleLoader__.load({
233
523
  onClick: () => void save(true),
234
524
  style: {
235
525
  padding: "7px 12px",
236
- borderRadius: 8
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"
237
532
  },
238
533
  children: t("overlay.saveClear")
239
534
  })
@@ -243,6 +538,281 @@ window.__ModuleLoader__.load({
243
538
  })
244
539
  });
245
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
+ `;
246
816
  // src/client/index.tsx
247
817
  /**
248
818
  * Browser half of the Quick Note example. React is provided by DSH at runtime.
@@ -260,30 +830,41 @@ window.__ModuleLoader__.load({
260
830
  }
261
831
  function Icon({ size = 18 }) {
262
832
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
263
- viewBox: "0 0 16 16",
833
+ viewBox: "0 0 24 24",
264
834
  width: size,
265
835
  height: size,
266
836
  fill: "none",
267
837
  "aria-hidden": "true",
268
838
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("rect", {
269
- x: "2.75",
270
- y: "2.75",
271
- width: "10.5",
272
- height: "10.5",
273
- rx: "2",
274
- stroke: "currentColor"
839
+ x: "4",
840
+ y: "3.5",
841
+ width: "16",
842
+ height: "17",
843
+ rx: "3",
844
+ stroke: "currentColor",
845
+ strokeWidth: "1.6"
275
846
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
276
- d: "M5.5 8h5",
847
+ d: "M8 8h8M8 12h8M8 16h5",
277
848
  stroke: "currentColor",
849
+ strokeWidth: "1.6",
278
850
  strokeLinecap: "round"
279
851
  })]
280
852
  });
281
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
+ }
282
859
  function Page({ t }) {
283
860
  const [notes, setNotes] = (0, react.useState)([]);
284
861
  const [text, setText] = (0, react.useState)("");
285
862
  const [error, setError] = (0, react.useState)("");
286
- const [undo, setUndo] = (0, react.useState)(null);
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)([]);
287
868
  async function load() {
288
869
  try {
289
870
  const data = await request("/notes");
@@ -299,8 +880,13 @@ window.__ModuleLoader__.load({
299
880
  return () => window.removeEventListener("dsh-quicknote:changed", changed);
300
881
  }, []);
301
882
  (0, react.useEffect)(() => {
302
- if (!undo) return;
303
- const timer = window.setTimeout(() => setUndo(null), 8e3);
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);
304
890
  return () => window.clearTimeout(timer);
305
891
  }, [undo]);
306
892
  async function add(event) {
@@ -309,7 +895,10 @@ window.__ModuleLoader__.load({
309
895
  try {
310
896
  await request("/notes/create", {
311
897
  method: "POST",
312
- body: JSON.stringify({ text })
898
+ body: JSON.stringify({
899
+ text,
900
+ origin: originOf(focusedSessionId())
901
+ })
313
902
  });
314
903
  setText("");
315
904
  setError("");
@@ -334,128 +923,295 @@ window.__ModuleLoader__.load({
334
923
  }
335
924
  async function remove(note) {
336
925
  try {
337
- await request("/notes/delete", {
926
+ const removed = (await request("/notes/delete", {
338
927
  method: "POST",
339
928
  body: JSON.stringify({ id: note.id })
340
- });
929
+ })).note;
930
+ if (removed) setUndo((current) => [...current, removed]);
931
+ setError("");
341
932
  await load();
342
933
  } catch (cause) {
343
934
  setError(String(cause.message));
344
935
  }
345
936
  }
346
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
347
- style: {
348
- padding: "24px 28px",
349
- maxWidth: 760,
350
- font: "inherit"
351
- },
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",
352
999
  children: [
353
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h1", {
354
- style: {
355
- fontSize: 22,
356
- margin: 0
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"));
357
1007
  },
1008
+ children: ["← ", backHint]
1009
+ }),
1010
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h1", {
1011
+ className: "qn-title",
358
1012
  children: t("panel.title")
359
1013
  }),
360
1014
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
361
- style: {
362
- margin: "6px 0 18px",
363
- color: "var(--dsw-alias-label-secondary)"
364
- },
1015
+ className: "qn-lede",
365
1016
  children: t("panel.body")
366
1017
  }),
367
1018
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("form", {
1019
+ className: "qn-compose",
368
1020
  onSubmit: add,
369
- style: {
370
- display: "flex",
371
- gap: 8,
372
- alignItems: "flex-start"
373
- },
374
1021
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
375
1022
  "data-testid": "note-input",
1023
+ className: "qn-textarea",
376
1024
  value: text,
377
1025
  onChange: (event) => setText(event.target.value),
378
1026
  placeholder: t("panel.placeholder"),
379
- rows: 3,
380
- style: {
381
- flex: 1,
382
- resize: "vertical",
383
- padding: 10,
384
- borderRadius: 8,
385
- border: "1px solid var(--dsw-alias-line, #d9dce1)"
386
- }
1027
+ rows: 3
387
1028
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
388
1029
  "data-testid": "note-add",
1030
+ className: "qn-button qn-primary",
389
1031
  type: "submit",
390
1032
  disabled: text.trim() === "",
391
- style: {
392
- padding: "9px 14px",
393
- borderRadius: 8
394
- },
395
1033
  children: t("panel.add")
396
1034
  })]
397
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
+ })] }),
398
1105
  error === "" ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
399
1106
  role: "alert",
400
- style: { color: "var(--dsw-alias-danger, #c0392b)" },
1107
+ className: "qn-alert",
401
1108
  children: error
402
1109
  }),
1110
+ notice === "" ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1111
+ role: "status",
1112
+ className: "qn-status",
1113
+ children: notice
1114
+ }),
403
1115
  notes.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
404
- style: {
405
- marginTop: 24,
406
- color: "var(--dsw-alias-label-secondary)"
407
- },
1116
+ className: "qn-empty",
408
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")
409
1121
  }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
410
- style: {
411
- listStyle: "none",
412
- padding: 0,
413
- margin: "24px 0 0",
414
- display: "grid",
415
- gap: 8
416
- },
417
- children: notes.map((note) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
418
- "data-testid": "note-item",
419
- style: {
420
- display: "flex",
421
- gap: 8,
422
- alignItems: "center",
423
- padding: 12,
424
- border: "1px solid var(--dsw-alias-line, #d9dce1)",
425
- borderRadius: 10
426
- },
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",
427
1192
  children: [
428
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
429
- style: {
430
- flex: 1,
431
- textDecoration: note.done ? "line-through" : "none",
432
- opacity: note.done ? .6 : 1
433
- },
434
- children: note.text
435
- }),
436
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
437
- type: "button",
438
- onClick: () => void toggle(note),
439
- style: {
440
- padding: "5px 9px",
441
- borderRadius: 7
442
- },
443
- children: note.done ? t("panel.reopen") : t("panel.done")
444
- }),
445
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
446
- type: "button",
447
- onClick: () => void remove(note),
448
- style: {
449
- padding: "5px 9px",
450
- borderRadius: 7
451
- },
452
- children: t("panel.delete")
453
- })
1193
+ undo.length === 1 ? t("panel.deleted") : t("panel.deletedCount").replace("{count}", String(undo.length)),
1194
+ " ",
1195
+ previewOf(undo[undo.length - 1].text)
454
1196
  ]
455
- }, note.id))
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
+ })]
456
1212
  })
457
1213
  ]
458
- });
1214
+ })] });
459
1215
  }
460
1216
  const name = NAME;
461
1217
  const inject = ["slots", "locale"];
@@ -464,41 +1220,107 @@ window.__ModuleLoader__.load({
464
1220
  ctx.effect(() => ctx.locale.register(NS, {
465
1221
  en: {
466
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.",
467
1226
  "panel.body": "Capture a note, mark it done, keep the list on one page.",
468
- "panel.placeholder": "Write a note…",
1227
+ "panel.placeholder": "Write a note… Use #tags to group it.",
469
1228
  "panel.add": "Add note",
470
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.",
471
1246
  "panel.done": "Done",
472
1247
  "panel.reopen": "Reopen",
473
1248
  "panel.delete": "Delete",
1249
+ "panel.deleteHint": "Delete this note. You can undo it right after.",
474
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",
475
1256
  "panel.completed": "Note updated.",
476
1257
  "panel.undo": "Undo",
477
- "toolbar.capture": "Note",
1258
+ "toolbar.capture": "Quick Note",
478
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.",
479
1263
  "overlay.cancel": "Cancel",
480
1264
  "overlay.save": "Save note",
481
1265
  "overlay.saveClear": "Save and clear draft",
482
- "overlay.empty": "Nothing to save."
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"
483
1272
  },
484
1273
  zh: {
485
1274
  "panel.title": "随手记",
1275
+ "panel.back": "返回对话",
1276
+ "panel.backTo": "返回「{title}」",
1277
+ "panel.backFailed": "打不开对话,请从左侧栏选择会话。",
486
1278
  "panel.body": "记下一条笔记,标记完成,在一页里管理。",
487
- "panel.placeholder": "写一条笔记…",
1279
+ "panel.placeholder": "写一条笔记… 用 #标签 归类。",
488
1280
  "panel.add": "添加",
489
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": "当前没有可用的输入框。",
490
1298
  "panel.done": "完成",
491
1299
  "panel.reopen": "重新打开",
492
1300
  "panel.delete": "删除",
1301
+ "panel.deleteHint": "删除这条笔记,随后可以撤销。",
493
1302
  "panel.deleted": "笔记已删除。",
1303
+ "panel.deletedCount": "已删除 {count} 条笔记。",
1304
+ "panel.deleteUndone": "笔记已恢复。",
1305
+ "panel.deletesUndone": "笔记已恢复。",
1306
+ "panel.undoAll": "全部撤销",
1307
+ "panel.dismiss": "关闭提示",
494
1308
  "panel.completed": "笔记已更新。",
495
1309
  "panel.undo": "撤销",
496
- "toolbar.capture": "记一下",
1310
+ "toolbar.capture": "随手记",
497
1311
  "overlay.title": "保存随手记",
1312
+ "overlay.viewAll": "查看全部",
1313
+ "overlay.viewAllCount": "查看全部({count})",
1314
+ "overlay.openFailed": "打不开笔记页,请从左侧栏的「随手记」进入。",
498
1315
  "overlay.cancel": "取消",
499
1316
  "overlay.save": "保存笔记",
500
1317
  "overlay.saveClear": "保存并清空输入框",
501
- "overlay.empty": "没有可保存的内容。"
1318
+ "overlay.empty": "没有可保存的内容。",
1319
+ "parked.saved": "已收进随手记",
1320
+ "parked.open": "打开列表",
1321
+ "parked.undo": "撤销",
1322
+ "parked.dismiss": "关闭提示",
1323
+ "shortcut.open": "打开随手记"
502
1324
  }
503
1325
  }), `${NAME}: dictionaries`);
504
1326
  ctx.slots.inject("sidebar.panellist", () => ctx.slots.register({
@@ -519,12 +1341,63 @@ window.__ModuleLoader__.load({
519
1341
  order: 10,
520
1342
  locale: NS
521
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));
522
1351
  ctx.slots.inject("shell.overlay", () => ctx.slots.register({
523
1352
  name: "shell.overlay",
524
1353
  id: "quicknote.overlay",
525
1354
  order: 40,
526
1355
  locale: NS
527
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
+ });
528
1401
  }
529
1402
  exports.apply = apply;
530
1403
  exports.inject = inject;