@elizaos/plugin-contacts 2.0.3-beta.2 → 2.0.3-beta.3

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 (59) hide show
  1. package/dist/components/ContactsAppView.d.ts +16 -0
  2. package/dist/components/ContactsAppView.d.ts.map +1 -0
  3. package/dist/components/ContactsAppView.helpers.d.ts +11 -0
  4. package/dist/components/ContactsAppView.helpers.d.ts.map +1 -0
  5. package/dist/components/ContactsAppView.helpers.js +36 -0
  6. package/dist/components/ContactsAppView.helpers.js.map +1 -0
  7. package/dist/components/ContactsAppView.interact.d.ts +2 -0
  8. package/dist/components/ContactsAppView.interact.d.ts.map +1 -0
  9. package/dist/components/ContactsAppView.interact.js +59 -0
  10. package/dist/components/ContactsAppView.interact.js.map +1 -0
  11. package/dist/components/ContactsAppView.js +747 -0
  12. package/dist/components/ContactsAppView.js.map +1 -0
  13. package/dist/components/ContactsSpatialView.d.ts +48 -0
  14. package/dist/components/ContactsSpatialView.d.ts.map +1 -0
  15. package/dist/components/ContactsSpatialView.js +254 -0
  16. package/dist/components/ContactsSpatialView.js.map +1 -0
  17. package/dist/components/ContactsView.d.ts +18 -0
  18. package/dist/components/ContactsView.d.ts.map +1 -0
  19. package/dist/components/ContactsView.js +163 -0
  20. package/dist/components/ContactsView.js.map +1 -0
  21. package/dist/components/contacts-app.d.ts +13 -0
  22. package/dist/components/contacts-app.d.ts.map +1 -0
  23. package/dist/components/contacts-app.js +20 -0
  24. package/dist/components/contacts-app.js.map +1 -0
  25. package/dist/components/contacts-view-bundle.d.ts +3 -0
  26. package/dist/components/contacts-view-bundle.d.ts.map +1 -0
  27. package/dist/components/contacts-view-bundle.js +7 -0
  28. package/dist/components/contacts-view-bundle.js.map +1 -0
  29. package/dist/index.d.ts +7 -0
  30. package/dist/index.d.ts.map +1 -0
  31. package/dist/index.js +20 -0
  32. package/dist/index.js.map +1 -0
  33. package/dist/plugin.d.ts +13 -0
  34. package/dist/plugin.d.ts.map +1 -0
  35. package/dist/plugin.js +32 -0
  36. package/dist/plugin.js.map +1 -0
  37. package/dist/providers/contacts.d.ts +11 -0
  38. package/dist/providers/contacts.d.ts.map +1 -0
  39. package/dist/providers/contacts.js +68 -0
  40. package/dist/providers/contacts.js.map +1 -0
  41. package/dist/register-terminal-view.d.ts +15 -0
  42. package/dist/register-terminal-view.d.ts.map +1 -0
  43. package/dist/register-terminal-view.js +21 -0
  44. package/dist/register-terminal-view.js.map +1 -0
  45. package/dist/register.d.ts +10 -0
  46. package/dist/register.d.ts.map +1 -0
  47. package/dist/register.js +10 -0
  48. package/dist/register.js.map +1 -0
  49. package/dist/ui.d.ts +4 -0
  50. package/dist/ui.d.ts.map +1 -0
  51. package/dist/ui.js +15 -0
  52. package/dist/ui.js.map +1 -0
  53. package/dist/views/bundle.js +467 -0
  54. package/dist/views/bundle.js.map +1 -0
  55. package/dist/views/dist-Cd2YtKy4.js +270 -0
  56. package/dist/views/dist-Cd2YtKy4.js.map +1 -0
  57. package/dist/views/web-DMSWpoWr.js +39 -0
  58. package/dist/views/web-DMSWpoWr.js.map +1 -0
  59. package/package.json +7 -6
@@ -0,0 +1,747 @@
1
+ import { jsx, jsxs } from "react/jsx-runtime";
2
+ import {
3
+ Contacts
4
+ } from "@elizaos/capacitor-contacts";
5
+ import { Button, Input } from "@elizaos/ui";
6
+ import { useAgentElement } from "@elizaos/ui/agent-surface";
7
+ import {
8
+ navigateToMessagesWithNumber,
9
+ navigateToPhoneWithNumber
10
+ } from "@elizaos/ui/app-navigate-view";
11
+ import { PermissionRecoveryCallout } from "@elizaos/ui/components";
12
+ import { isNative } from "@elizaos/ui/platform";
13
+ import {
14
+ ArrowLeft,
15
+ ChevronLeft,
16
+ Mail,
17
+ MessageSquareText,
18
+ Phone,
19
+ Plus,
20
+ Star,
21
+ Upload
22
+ } from "lucide-react";
23
+ import {
24
+ useCallback,
25
+ useEffect,
26
+ useId,
27
+ useMemo,
28
+ useRef,
29
+ useState
30
+ } from "react";
31
+ const EMPTY_FORM = {
32
+ displayName: "",
33
+ phoneNumber: "",
34
+ emailAddress: ""
35
+ };
36
+ function getInitials(name) {
37
+ const parts = name.trim().split(/\s+/).filter(Boolean);
38
+ if (parts.length === 0) return "?";
39
+ if (parts.length === 1) {
40
+ const first2 = parts[0];
41
+ return first2?.charAt(0).toUpperCase() ?? "?";
42
+ }
43
+ const first = parts[0]?.charAt(0) ?? "";
44
+ const last = parts[parts.length - 1]?.charAt(0) ?? "";
45
+ return `${first}${last}`.toUpperCase() || "?";
46
+ }
47
+ function dedupePreservingOrder(values) {
48
+ const seen = /* @__PURE__ */ new Set();
49
+ const result = [];
50
+ for (const value of values) {
51
+ if (seen.has(value)) continue;
52
+ seen.add(value);
53
+ result.push(value);
54
+ }
55
+ return result;
56
+ }
57
+ function isPermissionRecoveryError(message) {
58
+ const normalized = message.toLowerCase();
59
+ return normalized.includes("permission") || normalized.includes("denied") || normalized.includes("access is needed") || normalized.includes("read_contacts");
60
+ }
61
+ function ContactsAppView({ exitToApps, t }) {
62
+ const [contacts, setContacts] = useState([]);
63
+ const [loading, setLoading] = useState(true);
64
+ const [error, setError] = useState(null);
65
+ const [mode, setMode] = useState("list");
66
+ const [selectedId, setSelectedId] = useState(null);
67
+ const [form, setForm] = useState(EMPTY_FORM);
68
+ const [submitting, setSubmitting] = useState(false);
69
+ const fileInputRef = useRef(null);
70
+ const refresh = useCallback(async () => {
71
+ if (!isNative) {
72
+ setContacts([]);
73
+ setError(null);
74
+ setLoading(false);
75
+ return;
76
+ }
77
+ setLoading(true);
78
+ setError(null);
79
+ try {
80
+ const status = await Contacts.requestPermissions().catch(() => null);
81
+ if (status && status.contacts !== "granted") {
82
+ setContacts([]);
83
+ setError(
84
+ "Contacts access is needed to show your address book. Grant it in your device settings, then retry."
85
+ );
86
+ return;
87
+ }
88
+ const result = await Contacts.listContacts({});
89
+ setContacts(result.contacts);
90
+ } catch (err) {
91
+ setError(err instanceof Error ? err.message : String(err));
92
+ } finally {
93
+ setLoading(false);
94
+ }
95
+ }, []);
96
+ useEffect(() => {
97
+ void refresh();
98
+ const interval = setInterval(() => void refresh(), 2e4);
99
+ return () => clearInterval(interval);
100
+ }, [refresh]);
101
+ const selected = useMemo(
102
+ () => contacts.find((c) => c.id === selectedId) ?? null,
103
+ [contacts, selectedId]
104
+ );
105
+ const handleSelect = useCallback((id) => {
106
+ setSelectedId(id);
107
+ setMode("detail");
108
+ }, []);
109
+ const handleBackToList = useCallback(() => {
110
+ setMode("list");
111
+ setSelectedId(null);
112
+ }, []);
113
+ const handleOpenNew = useCallback(() => {
114
+ setForm(EMPTY_FORM);
115
+ setMode("new");
116
+ }, []);
117
+ const handleSubmitNew = useCallback(
118
+ async (event) => {
119
+ event.preventDefault();
120
+ const displayName = form.displayName.trim();
121
+ if (displayName.length === 0) return;
122
+ const payload = { displayName };
123
+ const phone = form.phoneNumber.trim();
124
+ const email = form.emailAddress.trim();
125
+ if (phone.length > 0) payload.phoneNumber = phone;
126
+ if (email.length > 0) payload.emailAddress = email;
127
+ setSubmitting(true);
128
+ setError(null);
129
+ try {
130
+ await Contacts.createContact(payload);
131
+ await refresh();
132
+ setMode("list");
133
+ setForm(EMPTY_FORM);
134
+ } catch (err) {
135
+ setError(err instanceof Error ? err.message : String(err));
136
+ } finally {
137
+ setSubmitting(false);
138
+ }
139
+ },
140
+ [form, refresh]
141
+ );
142
+ const handleImportClick = useCallback(() => {
143
+ fileInputRef.current?.click();
144
+ }, []);
145
+ const handleFileChange = useCallback(
146
+ async (event) => {
147
+ const file = event.target.files?.[0];
148
+ event.target.value = "";
149
+ if (!file) return;
150
+ setLoading(true);
151
+ setError(null);
152
+ try {
153
+ const vcardText = await file.text();
154
+ await Contacts.importVCard({ vcardText });
155
+ await refresh();
156
+ } catch (err) {
157
+ setError(err instanceof Error ? err.message : String(err));
158
+ setLoading(false);
159
+ }
160
+ },
161
+ [refresh]
162
+ );
163
+ const backLabel = mode === "list" ? t("nav.back", { defaultValue: "Back" }) : t("nav.backToList", { defaultValue: "Back to list" });
164
+ const back = useAgentElement({
165
+ id: "nav-back",
166
+ role: "button",
167
+ label: backLabel,
168
+ group: "contacts-nav",
169
+ description: mode === "list" ? "Leave the contacts app" : "Return to the contacts list"
170
+ });
171
+ const newLabel = t("contacts.new", { defaultValue: "New contact" });
172
+ const newEl = useAgentElement({
173
+ id: "action-new",
174
+ role: "button",
175
+ label: newLabel,
176
+ group: "contacts-actions",
177
+ description: "Open the new contact form"
178
+ });
179
+ return /* @__PURE__ */ jsxs(
180
+ "div",
181
+ {
182
+ "data-testid": "contacts-shell",
183
+ className: "fixed inset-0 z-50 flex h-[100vh] flex-col overflow-hidden bg-bg pb-[var(--safe-area-bottom,0px)] pl-[var(--safe-area-left,0px)] pr-[var(--safe-area-right,0px)] pt-[var(--safe-area-top,0px)] supports-[height:100dvh]:h-[100dvh]",
184
+ children: [
185
+ /* @__PURE__ */ jsx(
186
+ "input",
187
+ {
188
+ ref: fileInputRef,
189
+ type: "file",
190
+ accept: ".vcf,text/vcard,text/x-vcard",
191
+ className: "hidden",
192
+ onChange: handleFileChange
193
+ }
194
+ ),
195
+ /* @__PURE__ */ jsxs("header", { className: "flex shrink-0 items-center justify-between px-3 py-2", children: [
196
+ /* @__PURE__ */ jsxs("div", { className: "flex min-w-0 items-center gap-3", children: [
197
+ /* @__PURE__ */ jsx(
198
+ Button,
199
+ {
200
+ ref: back.ref,
201
+ ...back.agentProps,
202
+ variant: "ghost",
203
+ size: "icon",
204
+ className: "h-9 w-9 shrink-0 text-muted hover:text-txt",
205
+ onClick: mode === "list" ? exitToApps : handleBackToList,
206
+ "aria-label": backLabel,
207
+ children: mode === "list" ? /* @__PURE__ */ jsx(ArrowLeft, { className: "h-4 w-4" }) : /* @__PURE__ */ jsx(ChevronLeft, { className: "h-4 w-4" })
208
+ }
209
+ ),
210
+ /* @__PURE__ */ jsx("h1", { className: "truncate text-base font-semibold text-txt", children: mode === "detail" && selected ? selected.displayName : mode === "new" ? t("contacts.new", { defaultValue: "New contact" }) : t("contacts.title", { defaultValue: "Contacts" }) })
211
+ ] }),
212
+ mode === "list" && /* @__PURE__ */ jsx(
213
+ Button,
214
+ {
215
+ ref: newEl.ref,
216
+ ...newEl.agentProps,
217
+ variant: "ghost",
218
+ size: "icon",
219
+ className: "h-9 w-9 text-muted hover:text-txt",
220
+ onClick: handleOpenNew,
221
+ "aria-label": newLabel,
222
+ "data-testid": "contacts-new",
223
+ children: /* @__PURE__ */ jsx(Plus, { className: "h-4 w-4" })
224
+ }
225
+ )
226
+ ] }),
227
+ mode === "list" && /* @__PURE__ */ jsx("p", { "data-testid": "contacts-search-hint", className: "sr-only", children: t("contacts.searchHint", {
228
+ defaultValue: "Search contacts by typing in the chat."
229
+ }) }),
230
+ /* @__PURE__ */ jsxs("div", { className: "chat-native-scrollbar flex-1 overflow-y-auto", children: [
231
+ error && isPermissionRecoveryError(error) ? /* @__PURE__ */ jsx(
232
+ PermissionRecoveryCallout,
233
+ {
234
+ permission: "contacts",
235
+ title: t("contacts.permissionTitle", {
236
+ defaultValue: "Contacts access is off"
237
+ }),
238
+ description: error,
239
+ onRetry: refresh,
240
+ retryLabel: t("actions.retry", { defaultValue: "Try again" }),
241
+ className: "mx-4 mt-4",
242
+ testId: "contacts-permission-callout"
243
+ }
244
+ ) : error ? /* @__PURE__ */ jsx(
245
+ "div",
246
+ {
247
+ role: "alert",
248
+ className: "mx-4 mt-4 rounded-md border border-danger/40 bg-danger/10 px-3 py-2 text-sm text-danger",
249
+ children: error
250
+ }
251
+ ) : null,
252
+ mode === "list" && /* @__PURE__ */ jsx(
253
+ ContactList,
254
+ {
255
+ contacts,
256
+ loading: loading && contacts.length === 0,
257
+ empty: !loading && contacts.length === 0,
258
+ onSelect: handleSelect,
259
+ onImport: handleImportClick,
260
+ t
261
+ }
262
+ ),
263
+ mode === "detail" && selected && /* @__PURE__ */ jsx(ContactDetail, { contact: selected, t }),
264
+ mode === "new" && /* @__PURE__ */ jsx(
265
+ NewContactForm,
266
+ {
267
+ form,
268
+ submitting,
269
+ onChange: setForm,
270
+ onSubmit: handleSubmitNew,
271
+ onCancel: handleBackToList,
272
+ t
273
+ }
274
+ )
275
+ ] })
276
+ ]
277
+ }
278
+ );
279
+ }
280
+ function ContactList({
281
+ contacts,
282
+ loading,
283
+ empty,
284
+ onSelect,
285
+ onImport,
286
+ t
287
+ }) {
288
+ if (loading) {
289
+ return /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center py-16 text-sm text-muted", children: t("contacts.loading", { defaultValue: "Loading" }) });
290
+ }
291
+ if (empty) {
292
+ return /* @__PURE__ */ jsxs("div", { className: "mx-auto flex max-w-sm flex-col items-center gap-3 px-4 py-16 text-center", children: [
293
+ /* @__PURE__ */ jsx(
294
+ "span",
295
+ {
296
+ className: "flex h-16 w-16 items-center justify-center",
297
+ style: { background: "var(--accent-subtle)" },
298
+ children: /* @__PURE__ */ jsx(AddressBookMotif, {})
299
+ }
300
+ ),
301
+ /* @__PURE__ */ jsx("div", { className: "mt-2 text-base font-semibold text-txt", children: t("contacts.empty.title", { defaultValue: "None" }) }),
302
+ /* @__PURE__ */ jsx("p", { className: "sr-only", children: t("contacts.empty.body", {
303
+ defaultValue: "Import vCard or add contact."
304
+ }) }),
305
+ /* @__PURE__ */ jsx(ImportVCardButton, { onImport, t })
306
+ ] });
307
+ }
308
+ return /* @__PURE__ */ jsx("ul", { children: contacts.map((contact, index) => /* @__PURE__ */ jsx(
309
+ ContactListItem,
310
+ {
311
+ contact,
312
+ index,
313
+ onSelect,
314
+ t
315
+ },
316
+ contact.id
317
+ )) });
318
+ }
319
+ function AddressBookMotif() {
320
+ return /* @__PURE__ */ jsxs("svg", { width: "88", height: "88", viewBox: "0 0 88 88", fill: "none", role: "img", children: [
321
+ /* @__PURE__ */ jsx("title", { children: "Address book" }),
322
+ /* @__PURE__ */ jsx(
323
+ "rect",
324
+ {
325
+ x: "20",
326
+ y: "14",
327
+ width: "48",
328
+ height: "60",
329
+ rx: "10",
330
+ fill: "var(--surface)",
331
+ stroke: "var(--accent)",
332
+ strokeWidth: "2"
333
+ }
334
+ ),
335
+ /* @__PURE__ */ jsx(
336
+ "line",
337
+ {
338
+ x1: "20",
339
+ y1: "30",
340
+ x2: "14",
341
+ y2: "30",
342
+ stroke: "var(--accent)",
343
+ strokeWidth: "3",
344
+ strokeLinecap: "round"
345
+ }
346
+ ),
347
+ /* @__PURE__ */ jsx(
348
+ "line",
349
+ {
350
+ x1: "20",
351
+ y1: "44",
352
+ x2: "14",
353
+ y2: "44",
354
+ stroke: "var(--accent)",
355
+ strokeWidth: "3",
356
+ strokeLinecap: "round"
357
+ }
358
+ ),
359
+ /* @__PURE__ */ jsx(
360
+ "line",
361
+ {
362
+ x1: "20",
363
+ y1: "58",
364
+ x2: "14",
365
+ y2: "58",
366
+ stroke: "var(--accent)",
367
+ strokeWidth: "3",
368
+ strokeLinecap: "round"
369
+ }
370
+ ),
371
+ /* @__PURE__ */ jsx(
372
+ "circle",
373
+ {
374
+ cx: "44",
375
+ cy: "38",
376
+ r: "8",
377
+ fill: "var(--accent-subtle)",
378
+ stroke: "var(--accent)",
379
+ strokeWidth: "2"
380
+ }
381
+ ),
382
+ /* @__PURE__ */ jsx(
383
+ "path",
384
+ {
385
+ d: "M32 60 C32 51 56 51 56 60",
386
+ fill: "none",
387
+ stroke: "var(--accent)",
388
+ strokeWidth: "2",
389
+ strokeLinecap: "round"
390
+ }
391
+ )
392
+ ] });
393
+ }
394
+ function ImportVCardButton({ onImport, t }) {
395
+ const label = t("contacts.import", { defaultValue: "Import vCard" });
396
+ const { ref, agentProps } = useAgentElement({
397
+ id: "action-import",
398
+ role: "button",
399
+ label,
400
+ group: "contacts-actions",
401
+ description: "Import contacts from a vCard file"
402
+ });
403
+ return /* @__PURE__ */ jsxs(
404
+ Button,
405
+ {
406
+ ref,
407
+ ...agentProps,
408
+ variant: "default",
409
+ onClick: onImport,
410
+ className: "mt-2",
411
+ children: [
412
+ /* @__PURE__ */ jsx(Upload, { className: "mr-2 h-4 w-4" }),
413
+ label
414
+ ]
415
+ }
416
+ );
417
+ }
418
+ function ContactListItem({
419
+ contact,
420
+ index,
421
+ onSelect,
422
+ t
423
+ }) {
424
+ const name = contact.displayName || t("contacts.unnamed", { defaultValue: "Unnamed" });
425
+ const primaryPhone = contact.phoneNumbers[0] ?? "";
426
+ const primaryEmail = contact.emailAddresses[0] ?? "";
427
+ const subtitle = primaryPhone || primaryEmail;
428
+ const { ref, agentProps } = useAgentElement({
429
+ id: `contact-${contact.id}`,
430
+ role: "list-item",
431
+ label: name,
432
+ group: "contacts-list",
433
+ description: "Open this contact's details",
434
+ order: index
435
+ });
436
+ return /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs(
437
+ "button",
438
+ {
439
+ ref,
440
+ ...agentProps,
441
+ type: "button",
442
+ onClick: () => onSelect(contact.id),
443
+ className: "flex w-full items-center gap-3 px-3 py-2.5 text-left hover:bg-bg-accent/40 focus:bg-bg-accent/40 focus:outline-none",
444
+ children: [
445
+ /* @__PURE__ */ jsx(Avatar, { name: contact.displayName, photoUri: contact.photoUri }),
446
+ /* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
447
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5", children: [
448
+ /* @__PURE__ */ jsx("span", { className: "truncate text-sm font-medium text-txt", children: name }),
449
+ contact.starred && /* @__PURE__ */ jsx(
450
+ Star,
451
+ {
452
+ className: "h-3.5 w-3.5 shrink-0 text-[var(--accent)]",
453
+ fill: "currentColor",
454
+ "aria-label": t("contacts.starred", {
455
+ defaultValue: "Starred"
456
+ })
457
+ }
458
+ )
459
+ ] }),
460
+ subtitle && /* @__PURE__ */ jsx("div", { className: "truncate text-xs text-muted", children: subtitle })
461
+ ] })
462
+ ]
463
+ }
464
+ ) });
465
+ }
466
+ function ContactDetail({ contact, t }) {
467
+ return /* @__PURE__ */ jsxs("div", { className: "mx-auto flex max-w-xl flex-col gap-6 px-4 py-6", children: [
468
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center gap-3 text-center", children: [
469
+ /* @__PURE__ */ jsx(
470
+ Avatar,
471
+ {
472
+ name: contact.displayName,
473
+ photoUri: contact.photoUri,
474
+ size: "lg"
475
+ }
476
+ ),
477
+ /* @__PURE__ */ jsxs("div", { children: [
478
+ /* @__PURE__ */ jsx("h2", { className: "text-lg font-semibold text-txt", children: contact.displayName || t("contacts.unnamed", { defaultValue: "Unnamed" }) }),
479
+ contact.starred && /* @__PURE__ */ jsxs("div", { className: "mt-1 inline-flex items-center gap-1 text-xs text-[var(--accent)]", children: [
480
+ /* @__PURE__ */ jsx(Star, { className: "h-3 w-3", fill: "currentColor" }),
481
+ t("contacts.starred", { defaultValue: "Starred" })
482
+ ] })
483
+ ] })
484
+ ] }),
485
+ /* @__PURE__ */ jsx(
486
+ ContactFieldGroup,
487
+ {
488
+ label: t("contacts.phones", { defaultValue: "Phone" }),
489
+ items: contact.phoneNumbers,
490
+ renderItem: (value) => /* @__PURE__ */ jsx(ContactPhoneRow, { value, contactId: contact.id, t }),
491
+ emptyLabel: t("contacts.noPhones", {
492
+ defaultValue: "None"
493
+ })
494
+ }
495
+ ),
496
+ /* @__PURE__ */ jsx(
497
+ ContactFieldGroup,
498
+ {
499
+ label: t("contacts.emails", { defaultValue: "Email" }),
500
+ items: contact.emailAddresses,
501
+ renderItem: (value) => /* @__PURE__ */ jsxs(
502
+ "a",
503
+ {
504
+ href: `mailto:${value}`,
505
+ className: "flex items-center gap-2 text-sm text-txt hover:underline",
506
+ children: [
507
+ /* @__PURE__ */ jsx(Mail, { className: "h-4 w-4 text-muted" }),
508
+ /* @__PURE__ */ jsx("span", { className: "break-all", children: value })
509
+ ]
510
+ }
511
+ ),
512
+ emptyLabel: t("contacts.noEmails", {
513
+ defaultValue: "None"
514
+ })
515
+ }
516
+ ),
517
+ /* @__PURE__ */ jsx("p", { className: "sr-only", children: t("contacts.detail.readOnlyNote", {
518
+ defaultValue: "Editing existing contacts is unavailable on this device."
519
+ }) })
520
+ ] });
521
+ }
522
+ function ContactPhoneRow({
523
+ value,
524
+ contactId,
525
+ t
526
+ }) {
527
+ const callLabel = t("contacts.call", { defaultValue: "Call" });
528
+ const textLabel = t("contacts.text", { defaultValue: "Text" });
529
+ const callEl = useAgentElement({
530
+ id: `call-${contactId}-${value}`,
531
+ role: "button",
532
+ label: `${callLabel} ${value}`,
533
+ group: "contacts-detail-phone",
534
+ description: "Open the Phone dialer pre-filled with this number"
535
+ });
536
+ const textEl = useAgentElement({
537
+ id: `text-${contactId}-${value}`,
538
+ role: "button",
539
+ label: `${textLabel} ${value}`,
540
+ group: "contacts-detail-phone",
541
+ description: "Open Messages to text this number"
542
+ });
543
+ return /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
544
+ /* @__PURE__ */ jsx(Phone, { className: "h-4 w-4 shrink-0 text-muted" }),
545
+ /* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1 break-all text-sm text-txt", children: value }),
546
+ /* @__PURE__ */ jsx(
547
+ Button,
548
+ {
549
+ ref: callEl.ref,
550
+ ...callEl.agentProps,
551
+ variant: "ghost",
552
+ size: "icon",
553
+ className: "h-8 w-8 shrink-0 rounded-lg text-muted hover:text-txt",
554
+ onClick: () => navigateToPhoneWithNumber(value),
555
+ "aria-label": `${callLabel} ${value}`,
556
+ "data-testid": "contacts-detail-call",
557
+ children: /* @__PURE__ */ jsx(Phone, { className: "h-4 w-4" })
558
+ }
559
+ ),
560
+ /* @__PURE__ */ jsx(
561
+ Button,
562
+ {
563
+ ref: textEl.ref,
564
+ ...textEl.agentProps,
565
+ variant: "ghost",
566
+ size: "icon",
567
+ className: "h-8 w-8 shrink-0 rounded-lg text-muted hover:text-txt",
568
+ onClick: () => navigateToMessagesWithNumber(value),
569
+ "aria-label": `${textLabel} ${value}`,
570
+ "data-testid": "contacts-detail-text",
571
+ children: /* @__PURE__ */ jsx(MessageSquareText, { className: "h-4 w-4" })
572
+ }
573
+ )
574
+ ] });
575
+ }
576
+ function ContactFieldGroup({
577
+ label,
578
+ items,
579
+ renderItem,
580
+ emptyLabel
581
+ }) {
582
+ return /* @__PURE__ */ jsxs("section", { className: "flex flex-col gap-2 pt-2", children: [
583
+ /* @__PURE__ */ jsx("h3", { className: "text-sm font-medium text-muted", children: label }),
584
+ items.length === 0 ? /* @__PURE__ */ jsx("p", { className: "sr-only", children: emptyLabel }) : /* @__PURE__ */ jsx("ul", { className: "flex flex-col gap-2", children: dedupePreservingOrder(items).map((value) => /* @__PURE__ */ jsx("li", { children: renderItem(value) }, value)) })
585
+ ] });
586
+ }
587
+ function NewContactForm({
588
+ form,
589
+ submitting,
590
+ onChange,
591
+ onSubmit,
592
+ onCancel,
593
+ t
594
+ }) {
595
+ const canSubmit = form.displayName.trim().length > 0 && !submitting;
596
+ const nameId = useId();
597
+ const phoneId = useId();
598
+ const emailId = useId();
599
+ const nameEl = useAgentElement({
600
+ id: "input-name",
601
+ role: "text-input",
602
+ label: t("contacts.form.name", { defaultValue: "Name" }),
603
+ group: "contacts-form",
604
+ description: "Display name for the new contact"
605
+ });
606
+ const phoneEl = useAgentElement({
607
+ id: "input-phone",
608
+ role: "text-input",
609
+ label: t("contacts.form.phone", { defaultValue: "Phone" }),
610
+ group: "contacts-form",
611
+ description: "Phone number for the new contact"
612
+ });
613
+ const emailEl = useAgentElement({
614
+ id: "input-email",
615
+ role: "text-input",
616
+ label: t("contacts.form.email", { defaultValue: "Email" }),
617
+ group: "contacts-form",
618
+ description: "Email address for the new contact"
619
+ });
620
+ const cancelEl = useAgentElement({
621
+ id: "action-cancel",
622
+ role: "button",
623
+ label: t("actions.cancel", { defaultValue: "Cancel" }),
624
+ group: "contacts-form",
625
+ description: "Discard the new contact and return to the list"
626
+ });
627
+ const saveEl = useAgentElement({
628
+ id: "action-save",
629
+ role: "button",
630
+ label: t("contacts.form.save", { defaultValue: "Save" }),
631
+ group: "contacts-form",
632
+ description: "Save the new contact",
633
+ status: canSubmit ? void 0 : "disabled"
634
+ });
635
+ return /* @__PURE__ */ jsxs(
636
+ "form",
637
+ {
638
+ onSubmit,
639
+ className: "mx-auto flex max-w-md flex-col gap-3 px-3 py-4",
640
+ children: [
641
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1.5", children: [
642
+ /* @__PURE__ */ jsx("label", { htmlFor: nameId, className: "text-sm font-medium text-muted", children: t("contacts.form.name", { defaultValue: "Name" }) }),
643
+ /* @__PURE__ */ jsx(
644
+ Input,
645
+ {
646
+ ref: nameEl.ref,
647
+ ...nameEl.agentProps,
648
+ id: nameId,
649
+ value: form.displayName,
650
+ onChange: (e) => onChange({ ...form, displayName: e.target.value }),
651
+ placeholder: t("contacts.form.namePlaceholder", {
652
+ defaultValue: "Full name"
653
+ }),
654
+ required: true,
655
+ autoFocus: true
656
+ }
657
+ )
658
+ ] }),
659
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1.5", children: [
660
+ /* @__PURE__ */ jsx("label", { htmlFor: phoneId, className: "text-sm font-medium text-muted", children: t("contacts.form.phone", { defaultValue: "Phone" }) }),
661
+ /* @__PURE__ */ jsx(
662
+ Input,
663
+ {
664
+ ref: phoneEl.ref,
665
+ ...phoneEl.agentProps,
666
+ id: phoneId,
667
+ type: "tel",
668
+ inputMode: "tel",
669
+ value: form.phoneNumber,
670
+ onChange: (e) => onChange({ ...form, phoneNumber: e.target.value }),
671
+ placeholder: "+1 555 123 4567"
672
+ }
673
+ )
674
+ ] }),
675
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1.5", children: [
676
+ /* @__PURE__ */ jsx("label", { htmlFor: emailId, className: "text-sm font-medium text-muted", children: t("contacts.form.email", { defaultValue: "Email" }) }),
677
+ /* @__PURE__ */ jsx(
678
+ Input,
679
+ {
680
+ ref: emailEl.ref,
681
+ ...emailEl.agentProps,
682
+ id: emailId,
683
+ type: "email",
684
+ inputMode: "email",
685
+ value: form.emailAddress,
686
+ onChange: (e) => onChange({ ...form, emailAddress: e.target.value }),
687
+ placeholder: "name@example.com"
688
+ }
689
+ )
690
+ ] }),
691
+ /* @__PURE__ */ jsxs("div", { className: "mt-2 flex items-center justify-end gap-2", children: [
692
+ /* @__PURE__ */ jsx(
693
+ Button,
694
+ {
695
+ ref: cancelEl.ref,
696
+ ...cancelEl.agentProps,
697
+ type: "button",
698
+ variant: "ghost",
699
+ onClick: onCancel,
700
+ disabled: submitting,
701
+ children: t("actions.cancel", { defaultValue: "Cancel" })
702
+ }
703
+ ),
704
+ /* @__PURE__ */ jsx(
705
+ Button,
706
+ {
707
+ ref: saveEl.ref,
708
+ ...saveEl.agentProps,
709
+ type: "submit",
710
+ disabled: !canSubmit,
711
+ children: submitting ? t("contacts.form.saving", { defaultValue: "Saving\u2026" }) : t("contacts.form.save", { defaultValue: "Save" })
712
+ }
713
+ )
714
+ ] })
715
+ ]
716
+ }
717
+ );
718
+ }
719
+ function Avatar({
720
+ name,
721
+ photoUri,
722
+ size = "md"
723
+ }) {
724
+ const dimension = size === "lg" ? "h-16 w-16 text-xl" : "h-10 w-10 text-sm";
725
+ if (photoUri) {
726
+ return /* @__PURE__ */ jsx(
727
+ "img",
728
+ {
729
+ src: photoUri,
730
+ alt: "",
731
+ className: `${dimension} shrink-0 object-cover`
732
+ }
733
+ );
734
+ }
735
+ return /* @__PURE__ */ jsx(
736
+ "div",
737
+ {
738
+ "aria-hidden": "true",
739
+ className: `${dimension} flex shrink-0 items-center justify-center bg-bg-accent font-semibold text-muted`,
740
+ children: getInitials(name)
741
+ }
742
+ );
743
+ }
744
+ export {
745
+ ContactsAppView
746
+ };
747
+ //# sourceMappingURL=ContactsAppView.js.map