@bobfrankston/rmfmail 1.2.222 → 1.2.224

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.
Files changed (65) hide show
  1. package/bin/mailx.js +6 -9
  2. package/bin/mailx.js.map +1 -1
  3. package/bin/mailx.ts +6 -9
  4. package/client/android-bootstrap.bundle.js +38 -4
  5. package/client/android-bootstrap.bundle.js.map +2 -2
  6. package/client/app.bundle.js +149 -32
  7. package/client/app.bundle.js.map +2 -2
  8. package/client/components/address-book.js +93 -29
  9. package/client/components/address-book.js.map +1 -1
  10. package/client/components/address-book.ts +103 -24
  11. package/client/components/context-menu.js +76 -0
  12. package/client/components/context-menu.js.map +1 -1
  13. package/client/components/context-menu.ts +77 -0
  14. package/client/compose/compose.bundle.js +276 -116
  15. package/client/compose/compose.bundle.js.map +4 -4
  16. package/client/compose/compose.js +4 -149
  17. package/client/compose/compose.js.map +1 -1
  18. package/client/compose/compose.ts +4 -104
  19. package/client/compose/edit-commands.js +207 -0
  20. package/client/compose/edit-commands.js.map +1 -0
  21. package/client/compose/edit-commands.ts +181 -0
  22. package/client/compose/editor.js +6 -0
  23. package/client/compose/editor.js.map +1 -1
  24. package/client/compose/editor.ts +6 -0
  25. package/client/compose/spellcheck.js +19 -0
  26. package/client/compose/spellcheck.js.map +1 -1
  27. package/client/compose/spellcheck.ts +11 -0
  28. package/client/lib/api-client.js +2 -2
  29. package/client/lib/api-client.js.map +1 -1
  30. package/client/lib/api-client.ts +2 -2
  31. package/client/lib/mailxapi.js +2 -2
  32. package/client/styles/components.css +40 -0
  33. package/package.json +7 -7
  34. package/packages/mailx-imap/index.d.ts.map +1 -1
  35. package/packages/mailx-imap/index.js +32 -4
  36. package/packages/mailx-imap/index.js.map +1 -1
  37. package/packages/mailx-imap/index.ts +34 -5
  38. package/packages/mailx-imap/package-lock.json +2 -2
  39. package/packages/mailx-imap/package.json +1 -1
  40. package/packages/mailx-service/index.d.ts +1 -1
  41. package/packages/mailx-service/index.d.ts.map +1 -1
  42. package/packages/mailx-service/index.js +33 -4
  43. package/packages/mailx-service/index.js.map +1 -1
  44. package/packages/mailx-service/index.ts +28 -4
  45. package/packages/mailx-service/jsonrpc.js +1 -1
  46. package/packages/mailx-service/jsonrpc.js.map +1 -1
  47. package/packages/mailx-service/jsonrpc.ts +1 -1
  48. package/packages/mailx-service/package.json +1 -1
  49. package/packages/mailx-store/db.d.ts +30 -10
  50. package/packages/mailx-store/db.d.ts.map +1 -1
  51. package/packages/mailx-store/db.js +108 -10
  52. package/packages/mailx-store/db.js.map +1 -1
  53. package/packages/mailx-store/db.ts +126 -13
  54. package/packages/mailx-store/package.json +1 -1
  55. package/packages/mailx-store-web/android-bootstrap.d.ts.map +1 -1
  56. package/packages/mailx-store-web/android-bootstrap.js +17 -2
  57. package/packages/mailx-store-web/android-bootstrap.js.map +1 -1
  58. package/packages/mailx-store-web/android-bootstrap.ts +17 -2
  59. package/packages/mailx-store-web/db.d.ts +1 -1
  60. package/packages/mailx-store-web/db.d.ts.map +1 -1
  61. package/packages/mailx-store-web/db.js +17 -2
  62. package/packages/mailx-store-web/db.js.map +1 -1
  63. package/packages/mailx-store-web/db.ts +18 -3
  64. package/packages/mailx-store-web/package.json +1 -1
  65. /package/packages/mailx-imap/{node_modules.npmglobalize-stash-104584 → node_modules.npmglobalize-stash-142920}/.package-lock.json +0 -0
@@ -4,6 +4,17 @@
4
4
  * 'manual' (added directly here), 'google' (Google Contacts sync).
5
5
  */
6
6
  import { listContacts, upsertContact, deleteContact } from "../lib/api-client.js";
7
+ /** Editable card fields, in the order they appear in the edit dialog.
8
+ * `multiline` gets a textarea; `type` feeds the input element. */
9
+ const CONTACT_FIELDS = [
10
+ { key: "organization", label: "Company" },
11
+ { key: "title", label: "Job title" },
12
+ { key: "phone", label: "Phone", type: "tel" },
13
+ { key: "website", label: "Website", type: "url", placeholder: "https://" },
14
+ { key: "birthday", label: "Birthday", placeholder: "YYYY-MM-DD or --MM-DD" },
15
+ { key: "address", label: "Address", multiline: true },
16
+ { key: "notes", label: "Notes", multiline: true },
17
+ ];
7
18
  let isOpen = false;
8
19
  export async function openAddressBook(prefillSearch) {
9
20
  if (isOpen)
@@ -61,13 +72,13 @@ export async function openAddressBook(prefillSearch) {
61
72
  <span class="ab-actions"></span>
62
73
  </div>` + items.map(c => `
63
74
  <div class="ab-row" data-email="${escapeAttr(c.email)}">
64
- <span class="ab-name">${escapeHtml(c.name || "")}</span>
75
+ <span class="ab-name" title="${escapeAttr(cardSummary(c))}">${escapeHtml(c.name || "")}${cardSummary(c) ? ' <span class="ab-hascard" aria-hidden="true">•</span>' : ""}</span>
65
76
  <span class="ab-email">${escapeHtml(c.email)}</span>
66
77
  <span class="ab-source">${escapeHtml(c.source)}</span>
67
78
  <span class="ab-count-cell">${c.useCount || 0}</span>
68
79
  <span class="ab-last">${fmtDate(c.lastUsed)}</span>
69
80
  <span class="ab-actions">
70
- <button type="button" class="ab-edit" title="Edit name">✎</button>
81
+ <button type="button" class="ab-edit" title="Edit contact">✎</button>
71
82
  <button type="button" class="ab-del" title="Delete">🗑</button>
72
83
  <button type="button" class="ab-mail" title="Compose to">✉</button>
73
84
  </span>
@@ -80,40 +91,83 @@ export async function openAddressBook(prefillSearch) {
80
91
  row.querySelector(".ab-mail")?.addEventListener("click", () => composeTo(c));
81
92
  });
82
93
  };
83
- const beginEdit = (row, c) => {
94
+ // Edit opens the whole card, not just the name. The inline name-only input
95
+ // this replaced was the only editor there was, which is why phone, company,
96
+ // address and the rest had nowhere to be typed even once the store could
97
+ // hold them (Bob 2026-08-07: "extend the contact support for all fields").
98
+ const beginEdit = (_row, c) => {
84
99
  if (editingEmail === c.email)
85
100
  return;
86
101
  editingEmail = c.email;
87
- const nameSpan = row.querySelector(".ab-name");
88
- const original = c.name || "";
89
- nameSpan.innerHTML = `<input type="text" value="${escapeAttr(original)}" class="ab-name-input">`;
90
- const inp = nameSpan.querySelector("input");
91
- inp.focus();
92
- inp.select();
93
- const commit = async () => {
94
- const newName = inp.value.trim();
95
- if (newName !== original) {
96
- try {
97
- await upsertContact(newName, c.email);
98
- c.name = newName;
99
- }
100
- catch (e) {
101
- alert(`Update failed: ${e?.message || e}`);
102
- }
103
- }
104
- nameSpan.textContent = c.name || "";
102
+ const back = document.createElement("div");
103
+ back.className = "mailx-modal-backdrop";
104
+ const card = document.createElement("div");
105
+ card.className = "mailx-modal";
106
+ card.innerHTML = `
107
+ <div class="mailx-modal-title">
108
+ <span class="mailx-modal-title-text">Edit contact</span>
109
+ <button type="button" class="mailx-modal-close" data-role="x" title="Close (Esc)" aria-label="Close">&times;</button>
110
+ </div>
111
+ <div class="ab-card">
112
+ <label class="ab-field"><span>Name</span>
113
+ <input type="text" class="mailx-modal-input" data-field="name" value="${escapeAttr(c.name || "")}"></label>
114
+ <label class="ab-field"><span>Email</span>
115
+ <input type="email" class="mailx-modal-input" value="${escapeAttr(c.email)}" disabled
116
+ title="The address identifies the contact — delete and re-add to change it"></label>
117
+ ${CONTACT_FIELDS.map(f => `
118
+ <label class="ab-field"><span>${escapeHtml(f.label)}</span>
119
+ ${f.multiline
120
+ ? `<textarea class="mailx-modal-input" rows="2" data-field="${f.key}">${escapeHtml(c[f.key] || "")}</textarea>`
121
+ : `<input type="${f.type || "text"}" class="mailx-modal-input" data-field="${f.key}"
122
+ value="${escapeAttr(c[f.key] || "")}"
123
+ ${f.placeholder ? `placeholder="${escapeAttr(f.placeholder)}"` : ""}>`}
124
+ </label>`).join("")}
125
+ </div>
126
+ <div class="mailx-modal-buttons">
127
+ <span class="mailx-modal-spacer"></span>
128
+ <button type="button" class="mailx-modal-btn" data-role="cancel">Cancel</button>
129
+ <button type="button" class="mailx-modal-btn mailx-modal-btn-primary" data-role="save">Save</button>
130
+ </div>`;
131
+ back.appendChild(card);
132
+ document.body.appendChild(back);
133
+ card.querySelector('[data-field="name"]')?.focus();
134
+ const shut = () => {
135
+ back.remove();
136
+ document.removeEventListener("keydown", onCardKey, true);
105
137
  editingEmail = null;
106
138
  };
107
- inp.addEventListener("blur", commit);
108
- inp.addEventListener("keydown", (e) => {
109
- if (e.key === "Enter") {
139
+ // Capture phase + stopPropagation so Escape closes THIS dialog and not
140
+ // the address book behind it.
141
+ const onCardKey = (e) => {
142
+ if (e.key === "Escape") {
143
+ e.stopPropagation();
110
144
  e.preventDefault();
111
- inp.blur();
145
+ shut();
112
146
  }
113
- else if (e.key === "Escape") {
114
- e.preventDefault();
115
- inp.value = original;
116
- inp.blur();
147
+ };
148
+ document.addEventListener("keydown", onCardKey, true);
149
+ card.querySelector('[data-role="x"]')?.addEventListener("click", shut);
150
+ card.querySelector('[data-role="cancel"]')?.addEventListener("click", shut);
151
+ back.addEventListener("mousedown", (e) => { if (e.target === back)
152
+ shut(); });
153
+ card.querySelector('[data-role="save"]')?.addEventListener("click", async () => {
154
+ const val = (k) => card.querySelector(`[data-field="${k}"]`)?.value.trim() ?? "";
155
+ const fields = {};
156
+ for (const f of CONTACT_FIELDS)
157
+ fields[f.key] = val(f.key);
158
+ const newName = val("name");
159
+ const bd = fields.birthday;
160
+ if (bd && !/^(\d{4}|-)-\d{2}-\d{2}$/.test(bd)) {
161
+ alert(`Birthday must be YYYY-MM-DD, or --MM-DD when the year is unknown. Got "${bd}".`);
162
+ return;
163
+ }
164
+ try {
165
+ await upsertContact(newName, c.email, fields);
166
+ shut();
167
+ await reload();
168
+ }
169
+ catch (e) {
170
+ alert(`Update failed: ${e?.message || e}`);
117
171
  }
118
172
  });
119
173
  };
@@ -196,6 +250,16 @@ export async function openAddressBook(prefillSearch) {
196
250
  close(); });
197
251
  await reload();
198
252
  }
253
+ /** One-line digest of the card fields that are filled in — the row is too
254
+ * narrow for seven more columns, so this rides as the name's tooltip and
255
+ * drives the • marker that says "there is more here than a name". */
256
+ function cardSummary(c) {
257
+ return CONTACT_FIELDS
258
+ .map(f => ({ label: f.label, value: (c[f.key] || "").trim() }))
259
+ .filter(x => x.value)
260
+ .map(x => `${x.label}: ${x.value.replace(/\s+/g, " ")}`)
261
+ .join("\n");
262
+ }
199
263
  function escapeHtml(s) {
200
264
  return s.replace(/[&<>"']/g, c => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", "\"": "&quot;", "'": "&#39;" }[c]));
201
265
  }
@@ -1 +1 @@
1
- {"version":3,"file":"address-book.js","sourceRoot":"","sources":["address-book.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"address-book.js","sourceRoot":"","sources":["address-book.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAmBlF;mEACmE;AACnE,MAAM,cAAc,GAAoH;IACpI,EAAE,GAAG,EAAE,cAAc,EAAE,KAAK,EAAE,SAAS,EAAE;IACzC,EAAE,GAAG,EAAE,OAAO,EAAE,KAAK,EAAE,WAAW,EAAE;IACpC,EAAE,GAAG,EAAE,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE;IAC7C,EAAE,GAAG,EAAE,SAAS,EAAE,KAAK,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,WAAW,EAAE,UAAU,EAAE;IAC1E,EAAE,GAAG,EAAE,UAAU,EAAE,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,uBAAuB,EAAE;IAC5E,EAAE,GAAG,EAAE,SAAS,EAAE,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,IAAI,EAAE;IACrD,EAAE,GAAG,EAAE,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,SAAS,EAAE,IAAI,EAAE;CACpD,CAAC;AAEF,IAAI,MAAM,GAAG,KAAK,CAAC;AAEnB,MAAM,CAAC,KAAK,UAAU,eAAe,CAAC,aAAsB;IACxD,IAAI,MAAM;QAAE,OAAO;IACnB,MAAM,GAAG,IAAI,CAAC;IAEd,MAAM,QAAQ,GAAG,QAAQ,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC;IAC/C,QAAQ,CAAC,SAAS,GAAG,sBAAsB,CAAC;IAC5C,MAAM,KAAK,GAAG,QAAQ,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC;IAC5C,KAAK,CAAC,SAAS,GAAG,8BAA8B,CAAC;IACjD,KAAK,CAAC,SAAS,GAAG;;;;;;;;;;;;;;eAcP,CAAC;IACZ,QAAQ,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC;IAC5B,QAAQ,CAAC,IAAI,CAAC,WAAW,CAAC,QAAQ,CAAC,CAAC;IAEpC,MAAM,WAAW,GAAG,KAAK,CAAC,aAAa,CAAmB,YAAY,CAAE,CAAC;IACzE,MAAM,MAAM,GAAG,KAAK,CAAC,aAAa,CAAc,UAAU,CAAE,CAAC;IAC7D,MAAM,OAAO,GAAG,KAAK,CAAC,aAAa,CAAc,WAAW,CAAE,CAAC;IAE/D,IAAI,YAAY,GAAkB,IAAI,CAAC;IAEvC,MAAM,MAAM,GAAG,CAAC,KAAgB,EAAE,KAAa,EAAE,EAAE;QAC/C,OAAO,CAAC,WAAW,GAAG,KAAK,KAAK,KAAK,CAAC,MAAM;YACxC,CAAC,CAAC,GAAG,KAAK,WAAW,KAAK,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,EAAE;YAC7C,CAAC,CAAC,WAAW,KAAK,CAAC,MAAM,OAAO,KAAK,EAAE,CAAC;QAC5C,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACrB,MAAM,CAAC,SAAS,GAAG,gDAAgD,CAAC;YACpE,OAAO;QACX,CAAC;QACD,MAAM,OAAO,GAAG,CAAC,EAAU,EAAE,EAAE;YAC3B,IAAI,CAAC,EAAE;gBAAE,OAAO,EAAE,CAAC;YACnB,MAAM,CAAC,GAAG,IAAI,IAAI,CAAC,EAAE,CAAC,CAAC;YACvB,MAAM,GAAG,GAAG,IAAI,IAAI,EAAE,CAAC;YACvB,OAAO,CAAC,CAAC,WAAW,EAAE,KAAK,GAAG,CAAC,WAAW,EAAE;gBACxC,CAAC,CAAC,CAAC,CAAC,kBAAkB,CAAC,SAAS,EAAE,EAAE,KAAK,EAAE,OAAO,EAAE,GAAG,EAAE,SAAS,EAAE,CAAC;gBACrE,CAAC,CAAC,CAAC,CAAC,kBAAkB,EAAE,CAAC;QACjC,CAAC,CAAC;QACF,MAAM,CAAC,SAAS,GAAG;;;;;;;;mBAQR,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC;8CACS,UAAU,CAAC,CAAC,CAAC,KAAK,CAAC;+CAClB,UAAU,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,KAAK,UAAU,CAAC,CAAC,CAAC,IAAI,IAAI,EAAE,CAAC,GAAG,WAAW,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,uDAAuD,CAAC,CAAC,CAAC,EAAE;yCAC7I,UAAU,CAAC,CAAC,CAAC,KAAK,CAAC;0CAClB,UAAU,CAAC,CAAC,CAAC,MAAM,CAAC;8CAChB,CAAC,CAAC,QAAQ,IAAI,CAAC;wCACrB,OAAO,CAAC,CAAC,CAAC,QAAQ,CAAC;;;;;;mBAMxC,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QACtB,MAAM,CAAC,gBAAgB,CAAc,qBAAqB,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE;YACtE,MAAM,KAAK,GAAG,GAAG,CAAC,OAAO,CAAC,KAAM,CAAC;YACjC,MAAM,CAAC,GAAG,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,KAAK,KAAK,KAAK,CAAE,CAAC;YAC9C,GAAG,CAAC,aAAa,CAAC,UAAU,CAAC,EAAE,gBAAgB,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,SAAS,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,CAAC;YAClF,GAAG,CAAC,aAAa,CAAC,SAAS,CAAC,EAAE,gBAAgB,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC;YAC3E,GAAG,CAAC,aAAa,CAAC,UAAU,CAAC,EAAE,gBAAgB,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC;QACjF,CAAC,CAAC,CAAC;IACP,CAAC,CAAC;IAEF,2EAA2E;IAC3E,4EAA4E;IAC5E,yEAAyE;IACzE,2EAA2E;IAC3E,MAAM,SAAS,GAAG,CAAC,IAAiB,EAAE,CAAU,EAAE,EAAE;QAChD,IAAI,YAAY,KAAK,CAAC,CAAC,KAAK;YAAE,OAAO;QACrC,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC;QAEvB,MAAM,IAAI,GAAG,QAAQ,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC;QAC3C,IAAI,CAAC,SAAS,GAAG,sBAAsB,CAAC;QACxC,MAAM,IAAI,GAAG,QAAQ,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC;QAC3C,IAAI,CAAC,SAAS,GAAG,aAAa,CAAC;QAC/B,IAAI,CAAC,SAAS,GAAG;;;;;;;4FAOmE,UAAU,CAAC,CAAC,CAAC,IAAI,IAAI,EAAE,CAAC;;2EAEzC,UAAU,CAAC,CAAC,CAAC,KAAK,CAAC;;kBAE5E,cAAc,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC;gDACM,UAAU,CAAC,CAAC,CAAC,KAAK,CAAC;sBAC7C,CAAC,CAAC,SAAS;YACT,CAAC,CAAC,4DAA4D,CAAC,CAAC,GAAG,KAAK,UAAU,CAAE,CAAC,CAAC,CAAC,CAAC,GAAG,CAAY,IAAI,EAAE,CAAC,aAAa;YAC3H,CAAC,CAAC,gBAAgB,CAAC,CAAC,IAAI,IAAI,MAAM,2CAA2C,CAAC,CAAC,GAAG;2CAC/D,UAAU,CAAE,CAAC,CAAC,CAAC,CAAC,GAAG,CAAY,IAAI,EAAE,CAAC;oCAC7C,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,gBAAgB,UAAU,CAAC,CAAC,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,GAAG;yBAC/E,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC;;;;;;mBAMhB,CAAC;QACZ,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC;QACvB,QAAQ,CAAC,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC;QAChC,IAAI,CAAC,aAAa,CAAmB,qBAAqB,CAAC,EAAE,KAAK,EAAE,CAAC;QAErE,MAAM,IAAI,GAAG,GAAG,EAAE;YACd,IAAI,CAAC,MAAM,EAAE,CAAC;YACd,QAAQ,CAAC,mBAAmB,CAAC,SAAS,EAAE,SAAS,EAAE,IAAI,CAAC,CAAC;YACzD,YAAY,GAAG,IAAI,CAAC;QACxB,CAAC,CAAC;QACF,uEAAuE;QACvE,8BAA8B;QAC9B,MAAM,SAAS,GAAG,CAAC,CAAgB,EAAE,EAAE;YACnC,IAAI,CAAC,CAAC,GAAG,KAAK,QAAQ,EAAE,CAAC;gBAAC,CAAC,CAAC,eAAe,EAAE,CAAC;gBAAC,CAAC,CAAC,cAAc,EAAE,CAAC;gBAAC,IAAI,EAAE,CAAC;YAAC,CAAC;QAChF,CAAC,CAAC;QACF,QAAQ,CAAC,gBAAgB,CAAC,SAAS,EAAE,SAAS,EAAE,IAAI,CAAC,CAAC;QACtD,IAAI,CAAC,aAAa,CAAC,iBAAiB,CAAC,EAAE,gBAAgB,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC;QACvE,IAAI,CAAC,aAAa,CAAC,sBAAsB,CAAC,EAAE,gBAAgB,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC;QAC5E,IAAI,CAAC,gBAAgB,CAAC,WAAW,EAAE,CAAC,CAAC,EAAE,EAAE,GAAG,IAAI,CAAC,CAAC,MAAM,KAAK,IAAI;YAAE,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;QAE9E,IAAI,CAAC,aAAa,CAAC,oBAAoB,CAAC,EAAE,gBAAgB,CAAC,OAAO,EAAE,KAAK,IAAI,EAAE;YAC3E,MAAM,GAAG,GAAG,CAAC,CAAS,EAAE,EAAE,CAAC,IAAI,CAAC,aAAa,CAAyC,gBAAgB,CAAC,IAAI,CAAC,EAAE,KAAK,CAAC,IAAI,EAAE,IAAI,EAAE,CAAC;YACjI,MAAM,MAAM,GAA2B,EAAE,CAAC;YAC1C,KAAK,MAAM,CAAC,IAAI,cAAc;gBAAE,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;YAC3D,MAAM,OAAO,GAAG,GAAG,CAAC,MAAM,CAAC,CAAC;YAC5B,MAAM,EAAE,GAAG,MAAM,CAAC,QAAQ,CAAC;YAC3B,IAAI,EAAE,IAAI,CAAC,yBAAyB,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,CAAC;gBAC5C,KAAK,CAAC,0EAA0E,EAAE,IAAI,CAAC,CAAC;gBACxF,OAAO;YACX,CAAC;YACD,IAAI,CAAC;gBACD,MAAM,aAAa,CAAC,OAAO,EAAE,CAAC,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC;gBAC9C,IAAI,EAAE,CAAC;gBACP,MAAM,MAAM,EAAE,CAAC;YACnB,CAAC;YAAC,OAAO,CAAM,EAAE,CAAC;gBACd,KAAK,CAAC,kBAAkB,CAAC,EAAE,OAAO,IAAI,CAAC,EAAE,CAAC,CAAC;YAC/C,CAAC;QACL,CAAC,CAAC,CAAC;IACP,CAAC,CAAC;IAEF,MAAM,QAAQ,GAAG,KAAK,EAAE,CAAU,EAAE,EAAE;QAClC,IAAI,CAAC,OAAO,CAAC,mBAAmB,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC,KAAK,IAAI,CAAC;YAAE,OAAO;QAC/D,IAAI,CAAC;YACD,MAAM,aAAa,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC;YAC7B,MAAM,MAAM,EAAE,CAAC;QACnB,CAAC;QAAC,OAAO,CAAM,EAAE,CAAC;YACd,KAAK,CAAC,kBAAkB,CAAC,EAAE,OAAO,IAAI,CAAC,EAAE,CAAC,CAAC;QAC/C,CAAC;IACL,CAAC,CAAC;IAEF,MAAM,SAAS,GAAG,CAAC,CAAU,EAAE,EAAE;QAC7B,MAAM,IAAI,GAUN;YACA,IAAI,EAAE,KAAK;YACX,EAAE,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,IAAI,EAAE,EAAE,OAAO,EAAE,CAAC,CAAC,KAAK,EAAE,CAAC;YAC9C,EAAE,EAAE,EAAE,EAAE,GAAG,EAAE,EAAE,EAAE,OAAO,EAAE,EAAE,EAAE,QAAQ,EAAE,EAAE;YAC1C,SAAS,EAAE,EAAE,EAAE,UAAU,EAAE,EAAE,EAAE,QAAQ,EAAE,EAAE;SAC9C,CAAC;QACF,IAAI,CAAC;YAAC,cAAc,CAAC,OAAO,CAAC,aAAa,EAAE,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC;QAAC,CAAC;QAAC,MAAM,CAAC,CAAC,KAAK,CAAC,CAAC;QACpF,QAAQ,CAAC,aAAa,CAAC,IAAI,WAAW,CAAC,eAAe,EAAE,EAAE,MAAM,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC,CAAC,CAAC;QACtF,KAAK,EAAE,CAAC;IACZ,CAAC,CAAC;IAEF,IAAI,cAAkC,CAAC;IACvC,MAAM,MAAM,GAAG,KAAK,IAAI,EAAE;QACtB,IAAI,CAAC;YACD,MAAM,CAAC,GAAG,MAAM,YAAY,CAAC,WAAW,CAAC,KAAK,EAAE,CAAC,EAAE,GAAG,CAAC,CAAC;YACxD,MAAM,CAAC,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC;QAC7B,CAAC;QAAC,OAAO,CAAM,EAAE,CAAC;YACd,MAAM,CAAC,SAAS,GAAG,sCAAsC,UAAU,CAAC,CAAC,EAAE,OAAO,IAAI,MAAM,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC;QACzG,CAAC;IACL,CAAC,CAAC;IACF,MAAM,cAAc,GAAG,GAAG,EAAE;QACxB,IAAI,cAAc;YAAE,MAAM,CAAC,YAAY,CAAC,cAAc,CAAC,CAAC;QACxD,cAAc,GAAG,MAAM,CAAC,UAAU,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IACpD,CAAC,CAAC;IAEF,KAAK,CAAC,aAAa,CAAoB,SAAS,CAAC,EAAE,gBAAgB,CAAC,OAAO,EAAE,KAAK,IAAI,EAAE;QACpF,MAAM,KAAK,GAAG,MAAM,CAAC,gBAAgB,CAAC,CAAC;QACvC,IAAI,CAAC,KAAK;YAAE,OAAO;QACnB,MAAM,IAAI,GAAG,MAAM,CAAC,0BAA0B,CAAC,IAAI,EAAE,CAAC;QACtD,IAAI,CAAC;YACD,MAAM,aAAa,CAAC,IAAI,EAAE,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC;YACxC,MAAM,MAAM,EAAE,CAAC;QACnB,CAAC;QAAC,OAAO,CAAM,EAAE,CAAC;YACd,KAAK,CAAC,eAAe,CAAC,EAAE,OAAO,IAAI,CAAC,EAAE,CAAC,CAAC;QAC5C,CAAC;IACL,CAAC,CAAC,CAAC;IACH,WAAW,CAAC,gBAAgB,CAAC,OAAO,EAAE,cAAc,CAAC,CAAC;IAEtD,2EAA2E;IAC3E,4EAA4E;IAC5E,IAAI,aAAa;QAAE,WAAW,CAAC,KAAK,GAAG,aAAa,CAAC;IAErD,MAAM,KAAK,GAAG,GAAG,EAAE;QACf,IAAI,cAAc;YAAE,MAAM,CAAC,YAAY,CAAC,cAAc,CAAC,CAAC;QACxD,QAAQ,CAAC,MAAM,EAAE,CAAC;QAClB,QAAQ,CAAC,mBAAmB,CAAC,SAAS,EAAE,KAAK,EAAE,IAAI,CAAC,CAAC;QACrD,MAAM,GAAG,KAAK,CAAC;IACnB,CAAC,CAAC;IACF,MAAM,KAAK,GAAG,CAAC,CAAgB,EAAE,EAAE;QAC/B,IAAI,CAAC,CAAC,GAAG,KAAK,QAAQ,EAAE,CAAC;YAAC,CAAC,CAAC,eAAe,EAAE,CAAC;YAAC,CAAC,CAAC,cAAc,EAAE,CAAC;YAAC,KAAK,EAAE,CAAC;QAAC,CAAC;IACjF,CAAC,CAAC;IACF,QAAQ,CAAC,gBAAgB,CAAC,SAAS,EAAE,KAAK,EAAE,IAAI,CAAC,CAAC;IAClD,KAAK,CAAC,aAAa,CAAoB,WAAW,CAAE,CAAC,gBAAgB,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;IACtF,KAAK,CAAC,aAAa,CAAoB,uBAAuB,CAAE,CAAC,gBAAgB,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;IAClG,QAAQ,CAAC,gBAAgB,CAAC,WAAW,EAAE,CAAC,CAAC,EAAE,EAAE,GAAG,IAAI,CAAC,CAAC,MAAM,KAAK,QAAQ;QAAE,KAAK,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;IAEvF,MAAM,MAAM,EAAE,CAAC;AACnB,CAAC;AAED;;sEAEsE;AACtE,SAAS,WAAW,CAAC,CAAU;IAC3B,OAAO,cAAc;SAChB,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,KAAK,EAAE,KAAK,EAAE,CAAE,CAAC,CAAC,CAAC,CAAC,GAAG,CAAY,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;SAC1E,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC;SACpB,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,KAAK,KAAK,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,CAAC;SACvD,IAAI,CAAC,IAAI,CAAC,CAAC;AACpB,CAAC;AAED,SAAS,UAAU,CAAC,CAAS;IACzB,OAAO,CAAC,CAAC,OAAO,CAAC,UAAU,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,QAAQ,EAAE,GAAG,EAAE,OAAO,EAAE,CAAC,CAAC,CAAE,CAAC,CAAC,CAAC;AACtH,CAAC;AACD,SAAS,UAAU,CAAC,CAAS,IAAY,OAAO,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC"}
@@ -11,8 +11,29 @@ interface Contact {
11
11
  source: string;
12
12
  useCount: number;
13
13
  lastUsed: number;
14
+ // Full contact card (store v3). Google People carries all of these and
15
+ // mailx now keeps them instead of discarding everything but the name.
16
+ organization?: string;
17
+ title?: string;
18
+ phone?: string;
19
+ address?: string;
20
+ website?: string;
21
+ notes?: string;
22
+ birthday?: string;
14
23
  }
15
24
 
25
+ /** Editable card fields, in the order they appear in the edit dialog.
26
+ * `multiline` gets a textarea; `type` feeds the input element. */
27
+ const CONTACT_FIELDS: Array<{ key: keyof Contact & string; label: string; type?: string; multiline?: boolean; placeholder?: string }> = [
28
+ { key: "organization", label: "Company" },
29
+ { key: "title", label: "Job title" },
30
+ { key: "phone", label: "Phone", type: "tel" },
31
+ { key: "website", label: "Website", type: "url", placeholder: "https://" },
32
+ { key: "birthday", label: "Birthday", placeholder: "YYYY-MM-DD or --MM-DD" },
33
+ { key: "address", label: "Address", multiline: true },
34
+ { key: "notes", label: "Notes", multiline: true },
35
+ ];
36
+
16
37
  let isOpen = false;
17
38
 
18
39
  export async function openAddressBook(prefillSearch?: string): Promise<void> {
@@ -73,13 +94,13 @@ export async function openAddressBook(prefillSearch?: string): Promise<void> {
73
94
  <span class="ab-actions"></span>
74
95
  </div>` + items.map(c => `
75
96
  <div class="ab-row" data-email="${escapeAttr(c.email)}">
76
- <span class="ab-name">${escapeHtml(c.name || "")}</span>
97
+ <span class="ab-name" title="${escapeAttr(cardSummary(c))}">${escapeHtml(c.name || "")}${cardSummary(c) ? ' <span class="ab-hascard" aria-hidden="true">•</span>' : ""}</span>
77
98
  <span class="ab-email">${escapeHtml(c.email)}</span>
78
99
  <span class="ab-source">${escapeHtml(c.source)}</span>
79
100
  <span class="ab-count-cell">${c.useCount || 0}</span>
80
101
  <span class="ab-last">${fmtDate(c.lastUsed)}</span>
81
102
  <span class="ab-actions">
82
- <button type="button" class="ab-edit" title="Edit name">✎</button>
103
+ <button type="button" class="ab-edit" title="Edit contact">✎</button>
83
104
  <button type="button" class="ab-del" title="Delete">🗑</button>
84
105
  <button type="button" class="ab-mail" title="Compose to">✉</button>
85
106
  </span>
@@ -93,32 +114,79 @@ export async function openAddressBook(prefillSearch?: string): Promise<void> {
93
114
  });
94
115
  };
95
116
 
96
- const beginEdit = (row: HTMLElement, c: Contact) => {
117
+ // Edit opens the whole card, not just the name. The inline name-only input
118
+ // this replaced was the only editor there was, which is why phone, company,
119
+ // address and the rest had nowhere to be typed even once the store could
120
+ // hold them (Bob 2026-08-07: "extend the contact support for all fields").
121
+ const beginEdit = (_row: HTMLElement, c: Contact) => {
97
122
  if (editingEmail === c.email) return;
98
123
  editingEmail = c.email;
99
- const nameSpan = row.querySelector<HTMLElement>(".ab-name")!;
100
- const original = c.name || "";
101
- nameSpan.innerHTML = `<input type="text" value="${escapeAttr(original)}" class="ab-name-input">`;
102
- const inp = nameSpan.querySelector<HTMLInputElement>("input")!;
103
- inp.focus();
104
- inp.select();
105
- const commit = async () => {
106
- const newName = inp.value.trim();
107
- if (newName !== original) {
108
- try {
109
- await upsertContact(newName, c.email);
110
- c.name = newName;
111
- } catch (e: any) {
112
- alert(`Update failed: ${e?.message || e}`);
113
- }
114
- }
115
- nameSpan.textContent = c.name || "";
124
+
125
+ const back = document.createElement("div");
126
+ back.className = "mailx-modal-backdrop";
127
+ const card = document.createElement("div");
128
+ card.className = "mailx-modal";
129
+ card.innerHTML = `
130
+ <div class="mailx-modal-title">
131
+ <span class="mailx-modal-title-text">Edit contact</span>
132
+ <button type="button" class="mailx-modal-close" data-role="x" title="Close (Esc)" aria-label="Close">&times;</button>
133
+ </div>
134
+ <div class="ab-card">
135
+ <label class="ab-field"><span>Name</span>
136
+ <input type="text" class="mailx-modal-input" data-field="name" value="${escapeAttr(c.name || "")}"></label>
137
+ <label class="ab-field"><span>Email</span>
138
+ <input type="email" class="mailx-modal-input" value="${escapeAttr(c.email)}" disabled
139
+ title="The address identifies the contact — delete and re-add to change it"></label>
140
+ ${CONTACT_FIELDS.map(f => `
141
+ <label class="ab-field"><span>${escapeHtml(f.label)}</span>
142
+ ${f.multiline
143
+ ? `<textarea class="mailx-modal-input" rows="2" data-field="${f.key}">${escapeHtml((c[f.key] as string) || "")}</textarea>`
144
+ : `<input type="${f.type || "text"}" class="mailx-modal-input" data-field="${f.key}"
145
+ value="${escapeAttr((c[f.key] as string) || "")}"
146
+ ${f.placeholder ? `placeholder="${escapeAttr(f.placeholder)}"` : ""}>`}
147
+ </label>`).join("")}
148
+ </div>
149
+ <div class="mailx-modal-buttons">
150
+ <span class="mailx-modal-spacer"></span>
151
+ <button type="button" class="mailx-modal-btn" data-role="cancel">Cancel</button>
152
+ <button type="button" class="mailx-modal-btn mailx-modal-btn-primary" data-role="save">Save</button>
153
+ </div>`;
154
+ back.appendChild(card);
155
+ document.body.appendChild(back);
156
+ card.querySelector<HTMLInputElement>('[data-field="name"]')?.focus();
157
+
158
+ const shut = () => {
159
+ back.remove();
160
+ document.removeEventListener("keydown", onCardKey, true);
116
161
  editingEmail = null;
117
162
  };
118
- inp.addEventListener("blur", commit);
119
- inp.addEventListener("keydown", (e) => {
120
- if (e.key === "Enter") { e.preventDefault(); inp.blur(); }
121
- else if (e.key === "Escape") { e.preventDefault(); inp.value = original; inp.blur(); }
163
+ // Capture phase + stopPropagation so Escape closes THIS dialog and not
164
+ // the address book behind it.
165
+ const onCardKey = (e: KeyboardEvent) => {
166
+ if (e.key === "Escape") { e.stopPropagation(); e.preventDefault(); shut(); }
167
+ };
168
+ document.addEventListener("keydown", onCardKey, true);
169
+ card.querySelector('[data-role="x"]')?.addEventListener("click", shut);
170
+ card.querySelector('[data-role="cancel"]')?.addEventListener("click", shut);
171
+ back.addEventListener("mousedown", (e) => { if (e.target === back) shut(); });
172
+
173
+ card.querySelector('[data-role="save"]')?.addEventListener("click", async () => {
174
+ const val = (k: string) => card.querySelector<HTMLInputElement | HTMLTextAreaElement>(`[data-field="${k}"]`)?.value.trim() ?? "";
175
+ const fields: Record<string, string> = {};
176
+ for (const f of CONTACT_FIELDS) fields[f.key] = val(f.key);
177
+ const newName = val("name");
178
+ const bd = fields.birthday;
179
+ if (bd && !/^(\d{4}|-)-\d{2}-\d{2}$/.test(bd)) {
180
+ alert(`Birthday must be YYYY-MM-DD, or --MM-DD when the year is unknown. Got "${bd}".`);
181
+ return;
182
+ }
183
+ try {
184
+ await upsertContact(newName, c.email, fields);
185
+ shut();
186
+ await reload();
187
+ } catch (e: any) {
188
+ alert(`Update failed: ${e?.message || e}`);
189
+ }
122
190
  });
123
191
  };
124
192
 
@@ -202,6 +270,17 @@ export async function openAddressBook(prefillSearch?: string): Promise<void> {
202
270
  await reload();
203
271
  }
204
272
 
273
+ /** One-line digest of the card fields that are filled in — the row is too
274
+ * narrow for seven more columns, so this rides as the name's tooltip and
275
+ * drives the • marker that says "there is more here than a name". */
276
+ function cardSummary(c: Contact): string {
277
+ return CONTACT_FIELDS
278
+ .map(f => ({ label: f.label, value: ((c[f.key] as string) || "").trim() }))
279
+ .filter(x => x.value)
280
+ .map(x => `${x.label}: ${x.value.replace(/\s+/g, " ")}`)
281
+ .join("\n");
282
+ }
283
+
205
284
  function escapeHtml(s: string): string {
206
285
  return s.replace(/[&<>"']/g, c => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", "\"": "&quot;", "'": "&#39;" }[c]!));
207
286
  }
@@ -5,8 +5,18 @@
5
5
  let activeMenu = null;
6
6
  let dismissListener = null;
7
7
  let escapeListener = null;
8
+ /** Open submenu element, if any. Tracked separately from `activeMenu` so the
9
+ * outside-click dismisser doesn't treat the child as "outside" the parent. */
10
+ let activeSubmenu = null;
11
+ function closeSubmenu() {
12
+ if (activeSubmenu) {
13
+ activeSubmenu.remove();
14
+ activeSubmenu = null;
15
+ }
16
+ }
8
17
  /** Close any open context menu and remove dismiss listeners */
9
18
  export function closeContextMenu() {
19
+ closeSubmenu();
10
20
  if (activeMenu) {
11
21
  activeMenu.remove();
12
22
  activeMenu = null;
@@ -20,6 +30,50 @@ export function closeContextMenu() {
20
30
  escapeListener = null;
21
31
  }
22
32
  }
33
+ /** Render `items` as a child menu anchored to the right of `parentRow`,
34
+ * flipping to its left when there isn't room. Shares the .ctx-menu styling
35
+ * so it can't drift from the parent visually. */
36
+ function openSubmenu(parentRow, items) {
37
+ closeSubmenu();
38
+ const sub = document.createElement("div");
39
+ sub.className = "ctx-menu";
40
+ for (const item of items) {
41
+ if (item.separator) {
42
+ const sep = document.createElement("div");
43
+ sep.className = "ctx-sep";
44
+ sub.appendChild(sep);
45
+ continue;
46
+ }
47
+ const el = document.createElement("div");
48
+ el.className = "ctx-item" + (item.disabled ? " ctx-disabled" : "");
49
+ el.textContent = item.label;
50
+ if (item.tooltip)
51
+ el.title = item.tooltip;
52
+ if (!item.disabled) {
53
+ el.addEventListener("click", () => {
54
+ closeContextMenu();
55
+ item.action();
56
+ });
57
+ }
58
+ sub.appendChild(el);
59
+ }
60
+ document.body.appendChild(sub);
61
+ const anchor = parentRow.getBoundingClientRect();
62
+ const vw = window.visualViewport?.width ?? window.innerWidth;
63
+ const vh = window.visualViewport?.height ?? window.innerHeight;
64
+ const rect = sub.getBoundingClientRect();
65
+ // -2px overlap keeps the pointer inside a menu while travelling from the
66
+ // parent row into the child; a gap would fire the outside-click dismiss.
67
+ let left = anchor.right - 2;
68
+ if (left + rect.width > vw)
69
+ left = anchor.left - rect.width + 2;
70
+ let top = anchor.top;
71
+ if (top + rect.height > vh)
72
+ top = vh - rect.height - 4;
73
+ sub.style.left = `${Math.max(4, left)}px`;
74
+ sub.style.top = `${Math.max(4, top)}px`;
75
+ activeSubmenu = sub;
76
+ }
23
77
  /** Show a context menu at the given position */
24
78
  export function showContextMenu(x, y, items) {
25
79
  closeContextMenu();
@@ -39,6 +93,24 @@ export function showContextMenu(x, y, items) {
39
93
  el.textContent = item.label;
40
94
  if (item.tooltip)
41
95
  el.title = item.tooltip;
96
+ if (item.submenu && item.submenu.length > 0 && !item.disabled) {
97
+ // ▸ pushed to the right edge so parent rows read as parent rows.
98
+ el.style.display = "flex";
99
+ el.style.justifyContent = "space-between";
100
+ el.style.gap = "12px";
101
+ const caret = document.createElement("span");
102
+ caret.textContent = "▸";
103
+ caret.style.opacity = "0.6";
104
+ el.appendChild(caret);
105
+ const open = () => openSubmenu(el, item.submenu);
106
+ el.addEventListener("mouseenter", open);
107
+ el.addEventListener("click", (e) => { e.stopPropagation(); open(); });
108
+ menu.appendChild(el);
109
+ continue;
110
+ }
111
+ // Any non-parent row is hovered ⇒ whatever submenu was open belongs to
112
+ // a different row and should go.
113
+ el.addEventListener("mouseenter", closeSubmenu);
42
114
  if (!item.disabled) {
43
115
  el.addEventListener("click", () => {
44
116
  closeContextMenu();
@@ -78,6 +150,10 @@ export function showContextMenu(x, y, items) {
78
150
  // Deferred by one frame so the opening pointerdown doesn't immediately close it.
79
151
  requestAnimationFrame(() => {
80
152
  dismissListener = (e) => {
153
+ // A click in the SUBMENU is not a click outside — it lives in its
154
+ // own top-level element, so `activeMenu.contains` says otherwise.
155
+ if (activeSubmenu?.contains(e.target))
156
+ return;
81
157
  if (activeMenu && !activeMenu.contains(e.target)) {
82
158
  closeContextMenu();
83
159
  }
@@ -1 +1 @@
1
- {"version":3,"file":"context-menu.js","sourceRoot":"","sources":["context-menu.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"context-menu.js","sourceRoot":"","sources":["context-menu.ts"],"names":[],"mappings":"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"}
@@ -20,10 +20,24 @@ export interface MenuItem {
20
20
  * Trash, no undo" so the user can tell two near-identical entries
21
21
  * apart without trial-and-error. */
22
22
  tooltip?: string;
23
+ /** Nested items. Rendered with a ▸ affordance; opens on hover (and on
24
+ * click, so touch works). `action` is ignored when this is set — a
25
+ * parent row only opens its child menu. One level deep is deliberate:
26
+ * deeper nesting in a context menu is a usability tax, not a feature. */
27
+ submenu?: MenuItem[];
28
+ }
29
+
30
+ /** Open submenu element, if any. Tracked separately from `activeMenu` so the
31
+ * outside-click dismisser doesn't treat the child as "outside" the parent. */
32
+ let activeSubmenu: HTMLElement | null = null;
33
+
34
+ function closeSubmenu(): void {
35
+ if (activeSubmenu) { activeSubmenu.remove(); activeSubmenu = null; }
23
36
  }
24
37
 
25
38
  /** Close any open context menu and remove dismiss listeners */
26
39
  export function closeContextMenu(): void {
40
+ closeSubmenu();
27
41
  if (activeMenu) {
28
42
  activeMenu.remove();
29
43
  activeMenu = null;
@@ -38,6 +52,48 @@ export function closeContextMenu(): void {
38
52
  }
39
53
  }
40
54
 
55
+ /** Render `items` as a child menu anchored to the right of `parentRow`,
56
+ * flipping to its left when there isn't room. Shares the .ctx-menu styling
57
+ * so it can't drift from the parent visually. */
58
+ function openSubmenu(parentRow: HTMLElement, items: MenuItem[]): void {
59
+ closeSubmenu();
60
+ const sub = document.createElement("div");
61
+ sub.className = "ctx-menu";
62
+ for (const item of items) {
63
+ if (item.separator) {
64
+ const sep = document.createElement("div");
65
+ sep.className = "ctx-sep";
66
+ sub.appendChild(sep);
67
+ continue;
68
+ }
69
+ const el = document.createElement("div");
70
+ el.className = "ctx-item" + (item.disabled ? " ctx-disabled" : "");
71
+ el.textContent = item.label;
72
+ if (item.tooltip) el.title = item.tooltip;
73
+ if (!item.disabled) {
74
+ el.addEventListener("click", () => {
75
+ closeContextMenu();
76
+ item.action();
77
+ });
78
+ }
79
+ sub.appendChild(el);
80
+ }
81
+ document.body.appendChild(sub);
82
+ const anchor = parentRow.getBoundingClientRect();
83
+ const vw = window.visualViewport?.width ?? window.innerWidth;
84
+ const vh = window.visualViewport?.height ?? window.innerHeight;
85
+ const rect = sub.getBoundingClientRect();
86
+ // -2px overlap keeps the pointer inside a menu while travelling from the
87
+ // parent row into the child; a gap would fire the outside-click dismiss.
88
+ let left = anchor.right - 2;
89
+ if (left + rect.width > vw) left = anchor.left - rect.width + 2;
90
+ let top = anchor.top;
91
+ if (top + rect.height > vh) top = vh - rect.height - 4;
92
+ sub.style.left = `${Math.max(4, left)}px`;
93
+ sub.style.top = `${Math.max(4, top)}px`;
94
+ activeSubmenu = sub;
95
+ }
96
+
41
97
  /** Show a context menu at the given position */
42
98
  export function showContextMenu(x: number, y: number, items: MenuItem[]): void {
43
99
  closeContextMenu();
@@ -57,6 +113,24 @@ export function showContextMenu(x: number, y: number, items: MenuItem[]): void {
57
113
  if (item.emphasized) el.style.fontWeight = "600";
58
114
  el.textContent = item.label;
59
115
  if (item.tooltip) el.title = item.tooltip;
116
+ if (item.submenu && item.submenu.length > 0 && !item.disabled) {
117
+ // ▸ pushed to the right edge so parent rows read as parent rows.
118
+ el.style.display = "flex";
119
+ el.style.justifyContent = "space-between";
120
+ el.style.gap = "12px";
121
+ const caret = document.createElement("span");
122
+ caret.textContent = "▸";
123
+ caret.style.opacity = "0.6";
124
+ el.appendChild(caret);
125
+ const open = () => openSubmenu(el, item.submenu!);
126
+ el.addEventListener("mouseenter", open);
127
+ el.addEventListener("click", (e) => { e.stopPropagation(); open(); });
128
+ menu.appendChild(el);
129
+ continue;
130
+ }
131
+ // Any non-parent row is hovered ⇒ whatever submenu was open belongs to
132
+ // a different row and should go.
133
+ el.addEventListener("mouseenter", closeSubmenu);
60
134
  if (!item.disabled) {
61
135
  el.addEventListener("click", () => {
62
136
  closeContextMenu();
@@ -98,6 +172,9 @@ export function showContextMenu(x: number, y: number, items: MenuItem[]): void {
98
172
  // Deferred by one frame so the opening pointerdown doesn't immediately close it.
99
173
  requestAnimationFrame(() => {
100
174
  dismissListener = (e: Event) => {
175
+ // A click in the SUBMENU is not a click outside — it lives in its
176
+ // own top-level element, so `activeMenu.contains` says otherwise.
177
+ if (activeSubmenu?.contains(e.target as Node)) return;
101
178
  if (activeMenu && !activeMenu.contains(e.target as Node)) {
102
179
  closeContextMenu();
103
180
  }