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

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 @@
1
+ {"version":3,"sources":["../../src/components/ContactsAppView.tsx"],"sourcesContent":["/**\n * ContactsAppView — full-screen overlay app for the Android address book.\n *\n * Implements the OverlayApp Component contract. Backed by the\n * @elizaos/capacitor-contacts native plugin which exposes:\n * - listContacts({ query, limit })\n * - createContact({ displayName, phoneNumber(s), emailAddress(es) })\n * - importVCard({ vcardText })\n *\n * The native plugin does not currently expose update or delete, so the detail\n * panel is read-only; \"Edit\" creates a new contact entry rather than mutating\n * an existing row.\n */\n\nimport {\n type ContactSummary,\n Contacts,\n type CreateContactOptions,\n} from \"@elizaos/capacitor-contacts\";\nimport type { OverlayAppContext } from \"@elizaos/ui\";\nimport { Button, Input } from \"@elizaos/ui\";\nimport { useAgentElement } from \"@elizaos/ui/agent-surface\";\nimport {\n navigateToMessagesWithNumber,\n navigateToPhoneWithNumber,\n} from \"@elizaos/ui/app-navigate-view\";\nimport { PermissionRecoveryCallout } from \"@elizaos/ui/components\";\nimport { isNative } from \"@elizaos/ui/platform\";\nimport {\n ArrowLeft,\n ChevronLeft,\n Mail,\n MessageSquareText,\n Phone,\n Plus,\n Star,\n Upload,\n} from \"lucide-react\";\nimport {\n type ChangeEvent,\n type FormEvent,\n type ReactElement,\n useCallback,\n useEffect,\n useId,\n useMemo,\n useRef,\n useState,\n} from \"react\";\n\ntype Mode = \"list\" | \"detail\" | \"new\";\n\ntype NewContactForm = {\n displayName: string;\n phoneNumber: string;\n emailAddress: string;\n};\n\nconst EMPTY_FORM: NewContactForm = {\n displayName: \"\",\n phoneNumber: \"\",\n emailAddress: \"\",\n};\n\nfunction getInitials(name: string): string {\n const parts = name.trim().split(/\\s+/).filter(Boolean);\n if (parts.length === 0) return \"?\";\n if (parts.length === 1) {\n const first = parts[0];\n return first?.charAt(0).toUpperCase() ?? \"?\";\n }\n const first = parts[0]?.charAt(0) ?? \"\";\n const last = parts[parts.length - 1]?.charAt(0) ?? \"\";\n return `${first}${last}`.toUpperCase() || \"?\";\n}\n\nfunction dedupePreservingOrder(values: string[]): string[] {\n const seen = new Set<string>();\n const result: string[] = [];\n for (const value of values) {\n if (seen.has(value)) continue;\n seen.add(value);\n result.push(value);\n }\n return result;\n}\n\nfunction isPermissionRecoveryError(message: string): boolean {\n const normalized = message.toLowerCase();\n return (\n normalized.includes(\"permission\") ||\n normalized.includes(\"denied\") ||\n normalized.includes(\"access is needed\") ||\n normalized.includes(\"read_contacts\")\n );\n}\n\nexport function ContactsAppView({ exitToApps, t }: OverlayAppContext) {\n const [contacts, setContacts] = useState<ContactSummary[]>([]);\n const [loading, setLoading] = useState(true);\n const [error, setError] = useState<string | null>(null);\n const [mode, setMode] = useState<Mode>(\"list\");\n const [selectedId, setSelectedId] = useState<string | null>(null);\n const [form, setForm] = useState<NewContactForm>(EMPTY_FORM);\n const [submitting, setSubmitting] = useState(false);\n const fileInputRef = useRef<HTMLInputElement | null>(null);\n\n const refresh = useCallback(async () => {\n if (!isNative) {\n setContacts([]);\n setError(null);\n setLoading(false);\n return;\n }\n\n setLoading(true);\n setError(null);\n try {\n // Feature-gated permission: prompt for contacts access the first time the\n // Contacts view opens (idempotent — already-granted returns granted with\n // no prompt). Nothing requests this at app launch. Tolerates older bridges\n // without the request path by falling through to listContacts.\n const status = await Contacts.requestPermissions().catch(() => null);\n if (status && status.contacts !== \"granted\") {\n setContacts([]);\n setError(\n \"Contacts access is needed to show your address book. Grant it in your device settings, then retry.\",\n );\n return;\n }\n const result = await Contacts.listContacts({});\n setContacts(result.contacts);\n } catch (err) {\n setError(err instanceof Error ? err.message : String(err));\n } finally {\n setLoading(false);\n }\n }, []);\n\n // The native bridge has no change subscription, so keep the list fresh with a\n // quiet background poll (no user-facing Refresh control). Create/import still\n // re-load eagerly via `refresh()`.\n useEffect(() => {\n void refresh();\n const interval = setInterval(() => void refresh(), 20000);\n return () => clearInterval(interval);\n }, [refresh]);\n\n const selected = useMemo(\n () => contacts.find((c) => c.id === selectedId) ?? null,\n [contacts, selectedId],\n );\n\n const handleSelect = useCallback((id: string) => {\n setSelectedId(id);\n setMode(\"detail\");\n }, []);\n\n const handleBackToList = useCallback(() => {\n setMode(\"list\");\n setSelectedId(null);\n }, []);\n\n const handleOpenNew = useCallback(() => {\n setForm(EMPTY_FORM);\n setMode(\"new\");\n }, []);\n\n const handleSubmitNew = useCallback(\n async (event: FormEvent<HTMLFormElement>) => {\n event.preventDefault();\n const displayName = form.displayName.trim();\n if (displayName.length === 0) return;\n\n const payload: CreateContactOptions = { displayName };\n const phone = form.phoneNumber.trim();\n const email = form.emailAddress.trim();\n if (phone.length > 0) payload.phoneNumber = phone;\n if (email.length > 0) payload.emailAddress = email;\n\n setSubmitting(true);\n setError(null);\n try {\n await Contacts.createContact(payload);\n await refresh();\n setMode(\"list\");\n setForm(EMPTY_FORM);\n } catch (err) {\n setError(err instanceof Error ? err.message : String(err));\n } finally {\n setSubmitting(false);\n }\n },\n [form, refresh],\n );\n\n const handleImportClick = useCallback(() => {\n fileInputRef.current?.click();\n }, []);\n\n const handleFileChange = useCallback(\n async (event: ChangeEvent<HTMLInputElement>) => {\n const file = event.target.files?.[0];\n // Reset input so the same file can be re-selected later.\n event.target.value = \"\";\n if (!file) return;\n\n setLoading(true);\n setError(null);\n try {\n const vcardText = await file.text();\n await Contacts.importVCard({ vcardText });\n await refresh();\n } catch (err) {\n setError(err instanceof Error ? err.message : String(err));\n setLoading(false);\n }\n },\n [refresh],\n );\n\n const backLabel =\n mode === \"list\"\n ? t(\"nav.back\", { defaultValue: \"Back\" })\n : t(\"nav.backToList\", { defaultValue: \"Back to list\" });\n const back = useAgentElement<HTMLButtonElement>({\n id: \"nav-back\",\n role: \"button\",\n label: backLabel,\n group: \"contacts-nav\",\n description:\n mode === \"list\"\n ? \"Leave the contacts app\"\n : \"Return to the contacts list\",\n });\n const newLabel = t(\"contacts.new\", { defaultValue: \"New contact\" });\n const newEl = useAgentElement<HTMLButtonElement>({\n id: \"action-new\",\n role: \"button\",\n label: newLabel,\n group: \"contacts-actions\",\n description: \"Open the new contact form\",\n });\n\n return (\n <div\n data-testid=\"contacts-shell\"\n 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]\"\n >\n <input\n ref={fileInputRef}\n type=\"file\"\n accept=\".vcf,text/vcard,text/x-vcard\"\n className=\"hidden\"\n onChange={handleFileChange}\n />\n\n <header className=\"flex shrink-0 items-center justify-between px-3 py-2\">\n <div className=\"flex min-w-0 items-center gap-3\">\n <Button\n ref={back.ref}\n {...back.agentProps}\n variant=\"ghost\"\n size=\"icon\"\n className=\"h-9 w-9 shrink-0 text-muted hover:text-txt\"\n onClick={mode === \"list\" ? exitToApps : handleBackToList}\n aria-label={backLabel}\n >\n {mode === \"list\" ? (\n <ArrowLeft className=\"h-4 w-4\" />\n ) : (\n <ChevronLeft className=\"h-4 w-4\" />\n )}\n </Button>\n <h1 className=\"truncate text-base font-semibold text-txt\">\n {mode === \"detail\" && selected\n ? selected.displayName\n : mode === \"new\"\n ? t(\"contacts.new\", { defaultValue: \"New contact\" })\n : t(\"contacts.title\", { defaultValue: \"Contacts\" })}\n </h1>\n </div>\n\n {mode === \"list\" && (\n <Button\n ref={newEl.ref}\n {...newEl.agentProps}\n variant=\"ghost\"\n size=\"icon\"\n className=\"h-9 w-9 text-muted hover:text-txt\"\n onClick={handleOpenNew}\n aria-label={newLabel}\n data-testid=\"contacts-new\"\n >\n <Plus className=\"h-4 w-4\" />\n </Button>\n )}\n </header>\n\n {mode === \"list\" && (\n <p data-testid=\"contacts-search-hint\" className=\"sr-only\">\n {t(\"contacts.searchHint\", {\n defaultValue: \"Search contacts by typing in the chat.\",\n })}\n </p>\n )}\n\n <div className=\"chat-native-scrollbar flex-1 overflow-y-auto\">\n {error && isPermissionRecoveryError(error) ? (\n <PermissionRecoveryCallout\n permission=\"contacts\"\n title={t(\"contacts.permissionTitle\", {\n defaultValue: \"Contacts access is off\",\n })}\n description={error}\n onRetry={refresh}\n retryLabel={t(\"actions.retry\", { defaultValue: \"Try again\" })}\n className=\"mx-4 mt-4\"\n testId=\"contacts-permission-callout\"\n />\n ) : error ? (\n <div\n role=\"alert\"\n className=\"mx-4 mt-4 rounded-md border border-danger/40 bg-danger/10 px-3 py-2 text-sm text-danger\"\n >\n {error}\n </div>\n ) : null}\n\n {mode === \"list\" && (\n <ContactList\n contacts={contacts}\n loading={loading && contacts.length === 0}\n empty={!loading && contacts.length === 0}\n onSelect={handleSelect}\n onImport={handleImportClick}\n t={t}\n />\n )}\n\n {mode === \"detail\" && selected && (\n <ContactDetail contact={selected} t={t} />\n )}\n\n {mode === \"new\" && (\n <NewContactForm\n form={form}\n submitting={submitting}\n onChange={setForm}\n onSubmit={handleSubmitNew}\n onCancel={handleBackToList}\n t={t}\n />\n )}\n </div>\n </div>\n );\n}\n\ntype TFn = OverlayAppContext[\"t\"];\n\nfunction ContactList({\n contacts,\n loading,\n empty,\n onSelect,\n onImport,\n t,\n}: {\n contacts: ContactSummary[];\n loading: boolean;\n empty: boolean;\n onSelect: (id: string) => void;\n onImport: () => void;\n t: TFn;\n}) {\n if (loading) {\n return (\n <div className=\"flex items-center justify-center py-16 text-sm text-muted\">\n {t(\"contacts.loading\", { defaultValue: \"Loading\" })}\n </div>\n );\n }\n\n if (empty) {\n return (\n <div className=\"mx-auto flex max-w-sm flex-col items-center gap-3 px-4 py-16 text-center\">\n <span\n className=\"flex h-16 w-16 items-center justify-center\"\n style={{ background: \"var(--accent-subtle)\" }}\n >\n <AddressBookMotif />\n </span>\n <div className=\"mt-2 text-base font-semibold text-txt\">\n {t(\"contacts.empty.title\", { defaultValue: \"None\" })}\n </div>\n <p className=\"sr-only\">\n {t(\"contacts.empty.body\", {\n defaultValue: \"Import vCard or add contact.\",\n })}\n </p>\n <ImportVCardButton onImport={onImport} t={t} />\n </div>\n );\n }\n\n return (\n <ul>\n {contacts.map((contact, index) => (\n <ContactListItem\n key={contact.id}\n contact={contact}\n index={index}\n onSelect={onSelect}\n t={t}\n />\n ))}\n </ul>\n );\n}\n\nfunction AddressBookMotif() {\n return (\n <svg width=\"88\" height=\"88\" viewBox=\"0 0 88 88\" fill=\"none\" role=\"img\">\n <title>Address book</title>\n <rect\n x=\"20\"\n y=\"14\"\n width=\"48\"\n height=\"60\"\n rx=\"10\"\n fill=\"var(--surface)\"\n stroke=\"var(--accent)\"\n strokeWidth=\"2\"\n />\n <line\n x1=\"20\"\n y1=\"30\"\n x2=\"14\"\n y2=\"30\"\n stroke=\"var(--accent)\"\n strokeWidth=\"3\"\n strokeLinecap=\"round\"\n />\n <line\n x1=\"20\"\n y1=\"44\"\n x2=\"14\"\n y2=\"44\"\n stroke=\"var(--accent)\"\n strokeWidth=\"3\"\n strokeLinecap=\"round\"\n />\n <line\n x1=\"20\"\n y1=\"58\"\n x2=\"14\"\n y2=\"58\"\n stroke=\"var(--accent)\"\n strokeWidth=\"3\"\n strokeLinecap=\"round\"\n />\n <circle\n cx=\"44\"\n cy=\"38\"\n r=\"8\"\n fill=\"var(--accent-subtle)\"\n stroke=\"var(--accent)\"\n strokeWidth=\"2\"\n />\n <path\n d=\"M32 60 C32 51 56 51 56 60\"\n fill=\"none\"\n stroke=\"var(--accent)\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n />\n </svg>\n );\n}\n\nfunction ImportVCardButton({ onImport, t }: { onImport: () => void; t: TFn }) {\n const label = t(\"contacts.import\", { defaultValue: \"Import vCard\" });\n const { ref, agentProps } = useAgentElement<HTMLButtonElement>({\n id: \"action-import\",\n role: \"button\",\n label,\n group: \"contacts-actions\",\n description: \"Import contacts from a vCard file\",\n });\n return (\n <Button\n ref={ref}\n {...agentProps}\n variant=\"default\"\n onClick={onImport}\n className=\"mt-2\"\n >\n <Upload className=\"mr-2 h-4 w-4\" />\n {label}\n </Button>\n );\n}\n\nfunction ContactListItem({\n contact,\n index,\n onSelect,\n t,\n}: {\n contact: ContactSummary;\n index: number;\n onSelect: (id: string) => void;\n t: TFn;\n}) {\n const name =\n contact.displayName || t(\"contacts.unnamed\", { defaultValue: \"Unnamed\" });\n const primaryPhone = contact.phoneNumbers[0] ?? \"\";\n const primaryEmail = contact.emailAddresses[0] ?? \"\";\n const subtitle = primaryPhone || primaryEmail;\n const { ref, agentProps } = useAgentElement<HTMLButtonElement>({\n id: `contact-${contact.id}`,\n role: \"list-item\",\n label: name,\n group: \"contacts-list\",\n description: \"Open this contact's details\",\n order: index,\n });\n return (\n <li>\n <button\n ref={ref}\n {...agentProps}\n type=\"button\"\n onClick={() => onSelect(contact.id)}\n 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\"\n >\n <Avatar name={contact.displayName} photoUri={contact.photoUri} />\n <div className=\"min-w-0 flex-1\">\n <div className=\"flex items-center gap-1.5\">\n <span className=\"truncate text-sm font-medium text-txt\">\n {name}\n </span>\n {contact.starred && (\n <Star\n className=\"h-3.5 w-3.5 shrink-0 text-[var(--accent)]\"\n fill=\"currentColor\"\n aria-label={t(\"contacts.starred\", {\n defaultValue: \"Starred\",\n })}\n />\n )}\n </div>\n {subtitle && (\n <div className=\"truncate text-xs text-muted\">{subtitle}</div>\n )}\n </div>\n </button>\n </li>\n );\n}\n\nfunction ContactDetail({ contact, t }: { contact: ContactSummary; t: TFn }) {\n return (\n <div className=\"mx-auto flex max-w-xl flex-col gap-6 px-4 py-6\">\n <div className=\"flex flex-col items-center gap-3 text-center\">\n <Avatar\n name={contact.displayName}\n photoUri={contact.photoUri}\n size=\"lg\"\n />\n <div>\n <h2 className=\"text-lg font-semibold text-txt\">\n {contact.displayName ||\n t(\"contacts.unnamed\", { defaultValue: \"Unnamed\" })}\n </h2>\n {contact.starred && (\n <div className=\"mt-1 inline-flex items-center gap-1 text-xs text-[var(--accent)]\">\n <Star className=\"h-3 w-3\" fill=\"currentColor\" />\n {t(\"contacts.starred\", { defaultValue: \"Starred\" })}\n </div>\n )}\n </div>\n </div>\n\n <ContactFieldGroup\n label={t(\"contacts.phones\", { defaultValue: \"Phone\" })}\n items={contact.phoneNumbers}\n renderItem={(value) => (\n <ContactPhoneRow value={value} contactId={contact.id} t={t} />\n )}\n emptyLabel={t(\"contacts.noPhones\", {\n defaultValue: \"None\",\n })}\n />\n\n <ContactFieldGroup\n label={t(\"contacts.emails\", { defaultValue: \"Email\" })}\n items={contact.emailAddresses}\n renderItem={(value) => (\n <a\n href={`mailto:${value}`}\n className=\"flex items-center gap-2 text-sm text-txt hover:underline\"\n >\n <Mail className=\"h-4 w-4 text-muted\" />\n <span className=\"break-all\">{value}</span>\n </a>\n )}\n emptyLabel={t(\"contacts.noEmails\", {\n defaultValue: \"None\",\n })}\n />\n\n <p className=\"sr-only\">\n {t(\"contacts.detail.readOnlyNote\", {\n defaultValue:\n \"Editing existing contacts is unavailable on this device.\",\n })}\n </p>\n </div>\n );\n}\n\n// A phone-number row in the contact detail. Instead of an OS `tel:` handoff,\n// it links to the in-app Phone and Messages views via the navigation bus,\n// pre-seeding each with this number.\nfunction ContactPhoneRow({\n value,\n contactId,\n t,\n}: {\n value: string;\n contactId: string;\n t: TFn;\n}) {\n const callLabel = t(\"contacts.call\", { defaultValue: \"Call\" });\n const textLabel = t(\"contacts.text\", { defaultValue: \"Text\" });\n const callEl = useAgentElement<HTMLButtonElement>({\n id: `call-${contactId}-${value}`,\n role: \"button\",\n label: `${callLabel} ${value}`,\n group: \"contacts-detail-phone\",\n description: \"Open the Phone dialer pre-filled with this number\",\n });\n const textEl = useAgentElement<HTMLButtonElement>({\n id: `text-${contactId}-${value}`,\n role: \"button\",\n label: `${textLabel} ${value}`,\n group: \"contacts-detail-phone\",\n description: \"Open Messages to text this number\",\n });\n return (\n <div className=\"flex items-center gap-2\">\n <Phone className=\"h-4 w-4 shrink-0 text-muted\" />\n <span className=\"min-w-0 flex-1 break-all text-sm text-txt\">{value}</span>\n <Button\n ref={callEl.ref}\n {...callEl.agentProps}\n variant=\"ghost\"\n size=\"icon\"\n className=\"h-8 w-8 shrink-0 rounded-lg text-muted hover:text-txt\"\n onClick={() => navigateToPhoneWithNumber(value)}\n aria-label={`${callLabel} ${value}`}\n data-testid=\"contacts-detail-call\"\n >\n <Phone className=\"h-4 w-4\" />\n </Button>\n <Button\n ref={textEl.ref}\n {...textEl.agentProps}\n variant=\"ghost\"\n size=\"icon\"\n className=\"h-8 w-8 shrink-0 rounded-lg text-muted hover:text-txt\"\n onClick={() => navigateToMessagesWithNumber(value)}\n aria-label={`${textLabel} ${value}`}\n data-testid=\"contacts-detail-text\"\n >\n <MessageSquareText className=\"h-4 w-4\" />\n </Button>\n </div>\n );\n}\n\nfunction ContactFieldGroup({\n label,\n items,\n renderItem,\n emptyLabel,\n}: {\n label: string;\n items: string[];\n renderItem: (value: string) => ReactElement;\n emptyLabel: string;\n}) {\n return (\n <section className=\"flex flex-col gap-2 pt-2\">\n <h3 className=\"text-sm font-medium text-muted\">{label}</h3>\n {items.length === 0 ? (\n <p className=\"sr-only\">{emptyLabel}</p>\n ) : (\n <ul className=\"flex flex-col gap-2\">\n {dedupePreservingOrder(items).map((value) => (\n <li key={value}>{renderItem(value)}</li>\n ))}\n </ul>\n )}\n </section>\n );\n}\n\nfunction NewContactForm({\n form,\n submitting,\n onChange,\n onSubmit,\n onCancel,\n t,\n}: {\n form: NewContactForm;\n submitting: boolean;\n onChange: (next: NewContactForm) => void;\n onSubmit: (event: FormEvent<HTMLFormElement>) => void;\n onCancel: () => void;\n t: TFn;\n}) {\n const canSubmit = form.displayName.trim().length > 0 && !submitting;\n const nameId = useId();\n const phoneId = useId();\n const emailId = useId();\n\n const nameEl = useAgentElement<HTMLInputElement>({\n id: \"input-name\",\n role: \"text-input\",\n label: t(\"contacts.form.name\", { defaultValue: \"Name\" }),\n group: \"contacts-form\",\n description: \"Display name for the new contact\",\n });\n const phoneEl = useAgentElement<HTMLInputElement>({\n id: \"input-phone\",\n role: \"text-input\",\n label: t(\"contacts.form.phone\", { defaultValue: \"Phone\" }),\n group: \"contacts-form\",\n description: \"Phone number for the new contact\",\n });\n const emailEl = useAgentElement<HTMLInputElement>({\n id: \"input-email\",\n role: \"text-input\",\n label: t(\"contacts.form.email\", { defaultValue: \"Email\" }),\n group: \"contacts-form\",\n description: \"Email address for the new contact\",\n });\n const cancelEl = useAgentElement<HTMLButtonElement>({\n id: \"action-cancel\",\n role: \"button\",\n label: t(\"actions.cancel\", { defaultValue: \"Cancel\" }),\n group: \"contacts-form\",\n description: \"Discard the new contact and return to the list\",\n });\n const saveEl = useAgentElement<HTMLButtonElement>({\n id: \"action-save\",\n role: \"button\",\n label: t(\"contacts.form.save\", { defaultValue: \"Save\" }),\n group: \"contacts-form\",\n description: \"Save the new contact\",\n status: canSubmit ? undefined : \"disabled\",\n });\n\n return (\n <form\n onSubmit={onSubmit}\n className=\"mx-auto flex max-w-md flex-col gap-3 px-3 py-4\"\n >\n <div className=\"flex flex-col gap-1.5\">\n <label htmlFor={nameId} className=\"text-sm font-medium text-muted\">\n {t(\"contacts.form.name\", { defaultValue: \"Name\" })}\n </label>\n <Input\n ref={nameEl.ref}\n {...nameEl.agentProps}\n id={nameId}\n value={form.displayName}\n onChange={(e: ChangeEvent<HTMLInputElement>) =>\n onChange({ ...form, displayName: e.target.value })\n }\n placeholder={t(\"contacts.form.namePlaceholder\", {\n defaultValue: \"Full name\",\n })}\n required\n autoFocus\n />\n </div>\n\n <div className=\"flex flex-col gap-1.5\">\n <label htmlFor={phoneId} className=\"text-sm font-medium text-muted\">\n {t(\"contacts.form.phone\", { defaultValue: \"Phone\" })}\n </label>\n <Input\n ref={phoneEl.ref}\n {...phoneEl.agentProps}\n id={phoneId}\n type=\"tel\"\n inputMode=\"tel\"\n value={form.phoneNumber}\n onChange={(e: ChangeEvent<HTMLInputElement>) =>\n onChange({ ...form, phoneNumber: e.target.value })\n }\n placeholder=\"+1 555 123 4567\"\n />\n </div>\n\n <div className=\"flex flex-col gap-1.5\">\n <label htmlFor={emailId} className=\"text-sm font-medium text-muted\">\n {t(\"contacts.form.email\", { defaultValue: \"Email\" })}\n </label>\n <Input\n ref={emailEl.ref}\n {...emailEl.agentProps}\n id={emailId}\n type=\"email\"\n inputMode=\"email\"\n value={form.emailAddress}\n onChange={(e: ChangeEvent<HTMLInputElement>) =>\n onChange({ ...form, emailAddress: e.target.value })\n }\n placeholder=\"name@example.com\"\n />\n </div>\n\n <div className=\"mt-2 flex items-center justify-end gap-2\">\n <Button\n ref={cancelEl.ref}\n {...cancelEl.agentProps}\n type=\"button\"\n variant=\"ghost\"\n onClick={onCancel}\n disabled={submitting}\n >\n {t(\"actions.cancel\", { defaultValue: \"Cancel\" })}\n </Button>\n <Button\n ref={saveEl.ref}\n {...saveEl.agentProps}\n type=\"submit\"\n disabled={!canSubmit}\n >\n {submitting\n ? t(\"contacts.form.saving\", { defaultValue: \"Saving…\" })\n : t(\"contacts.form.save\", { defaultValue: \"Save\" })}\n </Button>\n </div>\n </form>\n );\n}\n\nfunction Avatar({\n name,\n photoUri,\n size = \"md\",\n}: {\n name: string;\n photoUri?: string;\n size?: \"md\" | \"lg\";\n}) {\n const dimension = size === \"lg\" ? \"h-16 w-16 text-xl\" : \"h-10 w-10 text-sm\";\n if (photoUri) {\n return (\n <img\n src={photoUri}\n alt=\"\"\n className={`${dimension} shrink-0 object-cover`}\n />\n );\n }\n return (\n <div\n aria-hidden=\"true\"\n className={`${dimension} flex shrink-0 items-center justify-center bg-bg-accent font-semibold text-muted`}\n >\n {getInitials(name)}\n </div>\n );\n}\n"],"mappings":"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","names":["first"]}
@@ -0,0 +1,48 @@
1
+ /**
2
+ * ContactsSpatialView - the contacts address book authored once with the
3
+ * spatial vocabulary, so it renders correctly wherever it is displayed:
4
+ *
5
+ * - GUI / XR - mounted in `<SpatialSurface>` (DOM; XR scales up).
6
+ * - TUI - rendered to real terminal lines by the agent terminal, via
7
+ * `registerSpatialTerminalView` (see `register-terminal-view.tsx`).
8
+ *
9
+ * It is purely presentational (a snapshot + an action callback in, primitives
10
+ * out) and imports only the cross-modality primitives plus a type-only view of
11
+ * the native contact record, so it is safe to render in the Node agent process
12
+ * where the terminal lives (no Capacitor runtime import).
13
+ */
14
+ import type { ContactSummary } from "@elizaos/capacitor-contacts";
15
+ /** Which screen the contacts surface is currently showing. */
16
+ export type ContactsMode = "list" | "detail" | "new";
17
+ /** Pending values for the new-contact form. */
18
+ export interface ContactsFormDraft {
19
+ displayName: string;
20
+ phoneNumber: string;
21
+ emailAddress: string;
22
+ }
23
+ export interface ContactsSnapshot {
24
+ /** Address book records (already filtered to the active query when present). */
25
+ contacts: ContactSummary[];
26
+ /** Active search filter; empty string when unfiltered. */
27
+ query: string;
28
+ /** Current screen. */
29
+ mode: ContactsMode;
30
+ /** Id of the contact shown in detail mode, if any. */
31
+ selectedId?: string | null;
32
+ /** Draft for the new-contact form (detail/new mode). */
33
+ form?: ContactsFormDraft;
34
+ loading?: boolean;
35
+ submitting?: boolean;
36
+ error?: string | null;
37
+ }
38
+ export interface ContactsSpatialViewProps {
39
+ snapshot: ContactsSnapshot;
40
+ /**
41
+ * Dispatch by agent id: `refresh`, `new`, `back`, `cancel`, `save`,
42
+ * `select:<id>`, `call:<value>`, `text:<value>`, and the field-edit signals
43
+ * `search:<value>`, `name:<value>`, `phone:<value>`, `email:<value>`.
44
+ */
45
+ onAction?: (action: string) => void;
46
+ }
47
+ export declare function ContactsSpatialView({ snapshot, onAction, }: ContactsSpatialViewProps): import("react/jsx-runtime").JSX.Element;
48
+ //# sourceMappingURL=ContactsSpatialView.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ContactsSpatialView.d.ts","sourceRoot":"","sources":["../../src/components/ContactsSpatialView.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAYlE,8DAA8D;AAC9D,MAAM,MAAM,YAAY,GAAG,MAAM,GAAG,QAAQ,GAAG,KAAK,CAAC;AAErD,+CAA+C;AAC/C,MAAM,WAAW,iBAAiB;IAChC,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,gBAAgB;IAC/B,gFAAgF;IAChF,QAAQ,EAAE,cAAc,EAAE,CAAC;IAC3B,0DAA0D;IAC1D,KAAK,EAAE,MAAM,CAAC;IACd,sBAAsB;IACtB,IAAI,EAAE,YAAY,CAAC;IACnB,sDAAsD;IACtD,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,wDAAwD;IACxD,IAAI,CAAC,EAAE,iBAAiB,CAAC;IACzB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACvB;AAkCD,MAAM,WAAW,wBAAwB;IACvC,QAAQ,EAAE,gBAAgB,CAAC;IAC3B;;;;OAIG;IACH,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;CACrC;AAED,wBAAgB,mBAAmB,CAAC,EAClC,QAAQ,EACR,QAAQ,GACT,EAAE,wBAAwB,2CAiE1B"}
@@ -0,0 +1,254 @@
1
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
2
+ import {
3
+ Button,
4
+ Card,
5
+ Divider,
6
+ Field,
7
+ HStack,
8
+ List,
9
+ Text,
10
+ VStack
11
+ } from "@elizaos/ui/spatial";
12
+ const EMPTY_FORM = {
13
+ displayName: "",
14
+ phoneNumber: "",
15
+ emailAddress: ""
16
+ };
17
+ function getInitials(name) {
18
+ const parts = name.trim().split(/\s+/).filter(Boolean);
19
+ if (parts.length === 0) return "?";
20
+ if (parts.length === 1) return (parts[0]?.charAt(0) ?? "?").toUpperCase();
21
+ const first = parts[0]?.charAt(0) ?? "";
22
+ const last = parts[parts.length - 1]?.charAt(0) ?? "";
23
+ return `${first}${last}`.toUpperCase() || "?";
24
+ }
25
+ function dedupePreservingOrder(values) {
26
+ const seen = /* @__PURE__ */ new Set();
27
+ const out = [];
28
+ for (const value of values) {
29
+ if (seen.has(value)) continue;
30
+ seen.add(value);
31
+ out.push(value);
32
+ }
33
+ return out;
34
+ }
35
+ function primaryLine(contact) {
36
+ return contact.phoneNumbers[0] ?? contact.emailAddresses[0] ?? "no contact method";
37
+ }
38
+ function ContactsSpatialView({
39
+ snapshot,
40
+ onAction
41
+ }) {
42
+ const dispatch = (action) => () => onAction?.(action);
43
+ const mode = snapshot.mode;
44
+ const selected = snapshot.selectedId != null ? snapshot.contacts.find((c) => c.id === snapshot.selectedId) ?? null : null;
45
+ return /* @__PURE__ */ jsxs(Card, { gap: 1, padding: 1, children: [
46
+ /* @__PURE__ */ jsxs(HStack, { gap: 1, align: "center", children: [
47
+ /* @__PURE__ */ jsx(Text, { style: "caption", tone: "muted", grow: 1, children: snapshot.loading ? "loading" : `${snapshot.contacts.length} contacts` }),
48
+ mode === "list" ? /* @__PURE__ */ jsxs(Fragment, { children: [
49
+ /* @__PURE__ */ jsx(
50
+ Button,
51
+ {
52
+ variant: "outline",
53
+ tone: "default",
54
+ agent: "refresh",
55
+ onPress: dispatch("refresh"),
56
+ children: "Refresh"
57
+ }
58
+ ),
59
+ /* @__PURE__ */ jsx(Button, { agent: "new", onPress: dispatch("new"), children: "New" })
60
+ ] }) : /* @__PURE__ */ jsx(
61
+ Button,
62
+ {
63
+ variant: "outline",
64
+ tone: "default",
65
+ agent: "back",
66
+ onPress: dispatch("back"),
67
+ children: "Back"
68
+ }
69
+ )
70
+ ] }),
71
+ snapshot.error ? /* @__PURE__ */ jsx(Text, { tone: "danger", style: "caption", children: snapshot.error }) : null,
72
+ mode === "list" ? /* @__PURE__ */ jsx(
73
+ ContactsListBody,
74
+ {
75
+ snapshot,
76
+ dispatch,
77
+ onAction
78
+ }
79
+ ) : mode === "detail" && selected ? /* @__PURE__ */ jsx(ContactsDetailBody, { contact: selected, dispatch }) : /* @__PURE__ */ jsx(
80
+ ContactsFormBody,
81
+ {
82
+ snapshot,
83
+ dispatch,
84
+ onAction
85
+ }
86
+ )
87
+ ] });
88
+ }
89
+ function ContactsListBody({
90
+ snapshot,
91
+ dispatch,
92
+ onAction
93
+ }) {
94
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
95
+ /* @__PURE__ */ jsx(
96
+ Field,
97
+ {
98
+ kind: "text",
99
+ label: "Search",
100
+ value: snapshot.query,
101
+ placeholder: "name, phone, or email",
102
+ agent: "search",
103
+ onChange: (value) => onAction?.(`search:${value}`)
104
+ }
105
+ ),
106
+ /* @__PURE__ */ jsx(Divider, { label: "address book" }),
107
+ snapshot.contacts.length === 0 ? /* @__PURE__ */ jsx(Text, { tone: "muted", align: "center", style: "caption", children: snapshot.loading ? "Loading" : "None" }) : /* @__PURE__ */ jsx(List, { gap: 0, children: snapshot.contacts.slice(0, 12).map((contact) => {
108
+ const name = contact.displayName || "Unnamed";
109
+ return /* @__PURE__ */ jsxs(
110
+ HStack,
111
+ {
112
+ gap: 1,
113
+ align: "center",
114
+ agent: `select-${contact.id}`,
115
+ children: [
116
+ /* @__PURE__ */ jsxs(Text, { tone: "muted", wrap: false, children: [
117
+ "[",
118
+ getInitials(name),
119
+ "]"
120
+ ] }),
121
+ /* @__PURE__ */ jsxs(VStack, { gap: 0, grow: 1, children: [
122
+ /* @__PURE__ */ jsx(Text, { bold: true, wrap: false, children: name }),
123
+ /* @__PURE__ */ jsx(Text, { style: "caption", tone: "muted", wrap: false, children: primaryLine(contact) })
124
+ ] }),
125
+ contact.starred ? /* @__PURE__ */ jsx(Text, { tone: "warning", wrap: false, children: "*" }) : null,
126
+ /* @__PURE__ */ jsx(
127
+ Button,
128
+ {
129
+ variant: "outline",
130
+ tone: "default",
131
+ agent: `select:${contact.id}`,
132
+ onPress: dispatch(`select:${contact.id}`),
133
+ children: "Open"
134
+ }
135
+ )
136
+ ]
137
+ },
138
+ contact.id
139
+ );
140
+ }) })
141
+ ] });
142
+ }
143
+ function ContactsDetailBody({
144
+ contact,
145
+ dispatch
146
+ }) {
147
+ const phones = dedupePreservingOrder(contact.phoneNumbers);
148
+ const emails = dedupePreservingOrder(contact.emailAddresses);
149
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
150
+ /* @__PURE__ */ jsxs(HStack, { gap: 1, align: "center", children: [
151
+ /* @__PURE__ */ jsxs(Text, { tone: "muted", children: [
152
+ "[",
153
+ getInitials(contact.displayName),
154
+ "]"
155
+ ] }),
156
+ /* @__PURE__ */ jsx(Text, { style: "subheading", bold: true, grow: 1, children: contact.displayName || "Unnamed" }),
157
+ contact.starred ? /* @__PURE__ */ jsx(Text, { tone: "warning", children: "starred" }) : null
158
+ ] }),
159
+ /* @__PURE__ */ jsx(Divider, { label: "phone" }),
160
+ phones.length === 0 ? /* @__PURE__ */ jsx(Text, { tone: "muted", style: "caption", children: "None" }) : /* @__PURE__ */ jsx(List, { gap: 0, children: phones.map((value) => /* @__PURE__ */ jsxs(HStack, { gap: 1, align: "center", children: [
161
+ /* @__PURE__ */ jsx(Text, { grow: 1, wrap: false, children: value }),
162
+ /* @__PURE__ */ jsx(
163
+ Button,
164
+ {
165
+ agent: `call:${value}`,
166
+ onPress: dispatch(`call:${value}`),
167
+ children: "Call"
168
+ }
169
+ ),
170
+ /* @__PURE__ */ jsx(
171
+ Button,
172
+ {
173
+ variant: "outline",
174
+ tone: "default",
175
+ agent: `text:${value}`,
176
+ onPress: dispatch(`text:${value}`),
177
+ children: "Text"
178
+ }
179
+ )
180
+ ] }, value)) }),
181
+ /* @__PURE__ */ jsx(Divider, { label: "email" }),
182
+ emails.length === 0 ? /* @__PURE__ */ jsx(Text, { tone: "muted", style: "caption", children: "None" }) : /* @__PURE__ */ jsx(List, { gap: 0, children: emails.map((value) => /* @__PURE__ */ jsx(Text, { wrap: false, children: value }, value)) }),
183
+ /* @__PURE__ */ jsx(Text, { tone: "muted", style: "caption", children: "Editing existing contacts is unavailable on this device." })
184
+ ] });
185
+ }
186
+ function ContactsFormBody({
187
+ snapshot,
188
+ dispatch,
189
+ onAction
190
+ }) {
191
+ const form = snapshot.form ?? EMPTY_FORM;
192
+ const canSubmit = form.displayName.trim().length > 0 && !snapshot.submitting;
193
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
194
+ /* @__PURE__ */ jsx(
195
+ Field,
196
+ {
197
+ kind: "text",
198
+ label: "Name",
199
+ value: form.displayName,
200
+ placeholder: "Full name",
201
+ agent: "name",
202
+ onChange: (value) => onAction?.(`name:${value}`)
203
+ }
204
+ ),
205
+ /* @__PURE__ */ jsx(
206
+ Field,
207
+ {
208
+ kind: "text",
209
+ label: "Phone",
210
+ value: form.phoneNumber,
211
+ placeholder: "+1 555 123 4567",
212
+ agent: "phone",
213
+ onChange: (value) => onAction?.(`phone:${value}`)
214
+ }
215
+ ),
216
+ /* @__PURE__ */ jsx(
217
+ Field,
218
+ {
219
+ kind: "text",
220
+ label: "Email",
221
+ value: form.emailAddress,
222
+ placeholder: "name@example.com",
223
+ agent: "email",
224
+ onChange: (value) => onAction?.(`email:${value}`)
225
+ }
226
+ ),
227
+ /* @__PURE__ */ jsxs(HStack, { gap: 1, children: [
228
+ /* @__PURE__ */ jsx(
229
+ Button,
230
+ {
231
+ grow: 1,
232
+ disabled: !canSubmit,
233
+ agent: "save",
234
+ onPress: dispatch("save"),
235
+ children: snapshot.submitting ? "Saving" : "Save"
236
+ }
237
+ ),
238
+ /* @__PURE__ */ jsx(
239
+ Button,
240
+ {
241
+ variant: "ghost",
242
+ tone: "danger",
243
+ agent: "cancel",
244
+ onPress: dispatch("cancel"),
245
+ children: "Cancel"
246
+ }
247
+ )
248
+ ] })
249
+ ] });
250
+ }
251
+ export {
252
+ ContactsSpatialView
253
+ };
254
+ //# sourceMappingURL=ContactsSpatialView.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/components/ContactsSpatialView.tsx"],"sourcesContent":["/**\n * ContactsSpatialView - the contacts address book authored once with the\n * spatial vocabulary, so it renders correctly wherever it is displayed:\n *\n * - GUI / XR - mounted in `<SpatialSurface>` (DOM; XR scales up).\n * - TUI - rendered to real terminal lines by the agent terminal, via\n * `registerSpatialTerminalView` (see `register-terminal-view.tsx`).\n *\n * It is purely presentational (a snapshot + an action callback in, primitives\n * out) and imports only the cross-modality primitives plus a type-only view of\n * the native contact record, so it is safe to render in the Node agent process\n * where the terminal lives (no Capacitor runtime import).\n */\n\nimport type { ContactSummary } from \"@elizaos/capacitor-contacts\";\nimport {\n Button,\n Card,\n Divider,\n Field,\n HStack,\n List,\n Text,\n VStack,\n} from \"@elizaos/ui/spatial\";\n\n/** Which screen the contacts surface is currently showing. */\nexport type ContactsMode = \"list\" | \"detail\" | \"new\";\n\n/** Pending values for the new-contact form. */\nexport interface ContactsFormDraft {\n displayName: string;\n phoneNumber: string;\n emailAddress: string;\n}\n\nexport interface ContactsSnapshot {\n /** Address book records (already filtered to the active query when present). */\n contacts: ContactSummary[];\n /** Active search filter; empty string when unfiltered. */\n query: string;\n /** Current screen. */\n mode: ContactsMode;\n /** Id of the contact shown in detail mode, if any. */\n selectedId?: string | null;\n /** Draft for the new-contact form (detail/new mode). */\n form?: ContactsFormDraft;\n loading?: boolean;\n submitting?: boolean;\n error?: string | null;\n}\n\nconst EMPTY_FORM: ContactsFormDraft = {\n displayName: \"\",\n phoneNumber: \"\",\n emailAddress: \"\",\n};\n\nfunction getInitials(name: string): string {\n const parts = name.trim().split(/\\s+/).filter(Boolean);\n if (parts.length === 0) return \"?\";\n if (parts.length === 1) return (parts[0]?.charAt(0) ?? \"?\").toUpperCase();\n const first = parts[0]?.charAt(0) ?? \"\";\n const last = parts[parts.length - 1]?.charAt(0) ?? \"\";\n return `${first}${last}`.toUpperCase() || \"?\";\n}\n\nfunction dedupePreservingOrder(values: string[]): string[] {\n const seen = new Set<string>();\n const out: string[] = [];\n for (const value of values) {\n if (seen.has(value)) continue;\n seen.add(value);\n out.push(value);\n }\n return out;\n}\n\nfunction primaryLine(contact: ContactSummary): string {\n return (\n contact.phoneNumbers[0] ?? contact.emailAddresses[0] ?? \"no contact method\"\n );\n}\n\nexport interface ContactsSpatialViewProps {\n snapshot: ContactsSnapshot;\n /**\n * Dispatch by agent id: `refresh`, `new`, `back`, `cancel`, `save`,\n * `select:<id>`, `call:<value>`, `text:<value>`, and the field-edit signals\n * `search:<value>`, `name:<value>`, `phone:<value>`, `email:<value>`.\n */\n onAction?: (action: string) => void;\n}\n\nexport function ContactsSpatialView({\n snapshot,\n onAction,\n}: ContactsSpatialViewProps) {\n const dispatch = (action: string) => () => onAction?.(action);\n const mode = snapshot.mode;\n const selected =\n snapshot.selectedId != null\n ? (snapshot.contacts.find((c) => c.id === snapshot.selectedId) ?? null)\n : null;\n\n return (\n <Card gap={1} padding={1}>\n <HStack gap={1} align=\"center\">\n <Text style=\"caption\" tone=\"muted\" grow={1}>\n {snapshot.loading\n ? \"loading\"\n : `${snapshot.contacts.length} contacts`}\n </Text>\n {mode === \"list\" ? (\n <>\n <Button\n variant=\"outline\"\n tone=\"default\"\n agent=\"refresh\"\n onPress={dispatch(\"refresh\")}\n >\n Refresh\n </Button>\n <Button agent=\"new\" onPress={dispatch(\"new\")}>\n New\n </Button>\n </>\n ) : (\n <Button\n variant=\"outline\"\n tone=\"default\"\n agent=\"back\"\n onPress={dispatch(\"back\")}\n >\n Back\n </Button>\n )}\n </HStack>\n\n {snapshot.error ? (\n <Text tone=\"danger\" style=\"caption\">\n {snapshot.error}\n </Text>\n ) : null}\n\n {mode === \"list\" ? (\n <ContactsListBody\n snapshot={snapshot}\n dispatch={dispatch}\n onAction={onAction}\n />\n ) : mode === \"detail\" && selected ? (\n <ContactsDetailBody contact={selected} dispatch={dispatch} />\n ) : (\n <ContactsFormBody\n snapshot={snapshot}\n dispatch={dispatch}\n onAction={onAction}\n />\n )}\n </Card>\n );\n}\n\nfunction ContactsListBody({\n snapshot,\n dispatch,\n onAction,\n}: {\n snapshot: ContactsSnapshot;\n dispatch: (action: string) => () => void;\n onAction?: (action: string) => void;\n}) {\n return (\n <>\n <Field\n kind=\"text\"\n label=\"Search\"\n value={snapshot.query}\n placeholder=\"name, phone, or email\"\n agent=\"search\"\n onChange={(value) => onAction?.(`search:${value}`)}\n />\n <Divider label=\"address book\" />\n {snapshot.contacts.length === 0 ? (\n <Text tone=\"muted\" align=\"center\" style=\"caption\">\n {snapshot.loading ? \"Loading\" : \"None\"}\n </Text>\n ) : (\n <List gap={0}>\n {snapshot.contacts.slice(0, 12).map((contact) => {\n const name = contact.displayName || \"Unnamed\";\n return (\n <HStack\n key={contact.id}\n gap={1}\n align=\"center\"\n agent={`select-${contact.id}`}\n >\n <Text tone=\"muted\" wrap={false}>\n [{getInitials(name)}]\n </Text>\n <VStack gap={0} grow={1}>\n <Text bold wrap={false}>\n {name}\n </Text>\n <Text style=\"caption\" tone=\"muted\" wrap={false}>\n {primaryLine(contact)}\n </Text>\n </VStack>\n {contact.starred ? (\n <Text tone=\"warning\" wrap={false}>\n *\n </Text>\n ) : null}\n <Button\n variant=\"outline\"\n tone=\"default\"\n agent={`select:${contact.id}`}\n onPress={dispatch(`select:${contact.id}`)}\n >\n Open\n </Button>\n </HStack>\n );\n })}\n </List>\n )}\n </>\n );\n}\n\nfunction ContactsDetailBody({\n contact,\n dispatch,\n}: {\n contact: ContactSummary;\n dispatch: (action: string) => () => void;\n}) {\n const phones = dedupePreservingOrder(contact.phoneNumbers);\n const emails = dedupePreservingOrder(contact.emailAddresses);\n return (\n <>\n <HStack gap={1} align=\"center\">\n <Text tone=\"muted\">[{getInitials(contact.displayName)}]</Text>\n <Text style=\"subheading\" bold grow={1}>\n {contact.displayName || \"Unnamed\"}\n </Text>\n {contact.starred ? <Text tone=\"warning\">starred</Text> : null}\n </HStack>\n\n <Divider label=\"phone\" />\n {phones.length === 0 ? (\n <Text tone=\"muted\" style=\"caption\">\n None\n </Text>\n ) : (\n <List gap={0}>\n {phones.map((value) => (\n <HStack key={value} gap={1} align=\"center\">\n <Text grow={1} wrap={false}>\n {value}\n </Text>\n <Button\n agent={`call:${value}`}\n onPress={dispatch(`call:${value}`)}\n >\n Call\n </Button>\n <Button\n variant=\"outline\"\n tone=\"default\"\n agent={`text:${value}`}\n onPress={dispatch(`text:${value}`)}\n >\n Text\n </Button>\n </HStack>\n ))}\n </List>\n )}\n\n <Divider label=\"email\" />\n {emails.length === 0 ? (\n <Text tone=\"muted\" style=\"caption\">\n None\n </Text>\n ) : (\n <List gap={0}>\n {emails.map((value) => (\n <Text key={value} wrap={false}>\n {value}\n </Text>\n ))}\n </List>\n )}\n\n <Text tone=\"muted\" style=\"caption\">\n Editing existing contacts is unavailable on this device.\n </Text>\n </>\n );\n}\n\nfunction ContactsFormBody({\n snapshot,\n dispatch,\n onAction,\n}: {\n snapshot: ContactsSnapshot;\n dispatch: (action: string) => () => void;\n onAction?: (action: string) => void;\n}) {\n const form = snapshot.form ?? EMPTY_FORM;\n const canSubmit = form.displayName.trim().length > 0 && !snapshot.submitting;\n return (\n <>\n <Field\n kind=\"text\"\n label=\"Name\"\n value={form.displayName}\n placeholder=\"Full name\"\n agent=\"name\"\n onChange={(value) => onAction?.(`name:${value}`)}\n />\n <Field\n kind=\"text\"\n label=\"Phone\"\n value={form.phoneNumber}\n placeholder=\"+1 555 123 4567\"\n agent=\"phone\"\n onChange={(value) => onAction?.(`phone:${value}`)}\n />\n <Field\n kind=\"text\"\n label=\"Email\"\n value={form.emailAddress}\n placeholder=\"name@example.com\"\n agent=\"email\"\n onChange={(value) => onAction?.(`email:${value}`)}\n />\n <HStack gap={1}>\n <Button\n grow={1}\n disabled={!canSubmit}\n agent=\"save\"\n onPress={dispatch(\"save\")}\n >\n {snapshot.submitting ? \"Saving\" : \"Save\"}\n </Button>\n <Button\n variant=\"ghost\"\n tone=\"danger\"\n agent=\"cancel\"\n onPress={dispatch(\"cancel\")}\n >\n Cancel\n </Button>\n </HStack>\n </>\n );\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,18 @@
1
+ /**
2
+ * ContactsView — the single GUI/XR data wrapper for the Contacts surface.
3
+ *
4
+ * It owns the live Android address-book data (contact fetch, permission gate,
5
+ * background poll, list/detail/new mode, the new-contact form, and the
6
+ * Call/Text handoffs to the Phone/Messages views) and renders the one
7
+ * presentational {@link ContactsSpatialView} inside a {@link SpatialSurface}.
8
+ * Omitting the `modality` prop lets `SpatialSurface` auto-detect GUI vs XR via
9
+ * `window.__elizaXRContext`, so the SAME component serves both surfaces. The
10
+ * TUI surface renders the same `ContactsSpatialView` through the terminal
11
+ * registry (see `register-terminal-view.tsx`).
12
+ *
13
+ * The full-screen overlay-app variant (with vCard import + permission-recovery
14
+ * callout) remains `ContactsAppView`, loaded by the overlay-app registry on
15
+ * ElizaOS; this wrapper is the cross-modality view-bundle export.
16
+ */
17
+ export declare function ContactsView(): import("react/jsx-runtime").JSX.Element;
18
+ //# sourceMappingURL=ContactsView.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ContactsView.d.ts","sourceRoot":"","sources":["../../src/components/ContactsView.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AA4BH,wBAAgB,YAAY,4CA0J3B"}
@@ -0,0 +1,163 @@
1
+ import { jsx } from "react/jsx-runtime";
2
+ import {
3
+ Contacts
4
+ } from "@elizaos/capacitor-contacts";
5
+ import {
6
+ navigateToMessagesWithNumber,
7
+ navigateToPhoneWithNumber
8
+ } from "@elizaos/ui/app-navigate-view";
9
+ import { isNative } from "@elizaos/ui/platform";
10
+ import { SpatialSurface } from "@elizaos/ui/spatial";
11
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
12
+ import { matchesQuery } from "./ContactsAppView.helpers.js";
13
+ import {
14
+ ContactsSpatialView
15
+ } from "./ContactsSpatialView.js";
16
+ const EMPTY_FORM = {
17
+ displayName: "",
18
+ phoneNumber: "",
19
+ emailAddress: ""
20
+ };
21
+ function ContactsView() {
22
+ const [contacts, setContacts] = useState([]);
23
+ const [query, setQuery] = useState("");
24
+ const [mode, setMode] = useState("list");
25
+ const [selectedId, setSelectedId] = useState(null);
26
+ const [form, setForm] = useState(EMPTY_FORM);
27
+ const [loading, setLoading] = useState(false);
28
+ const [submitting, setSubmitting] = useState(false);
29
+ const [error, setError] = useState(null);
30
+ const refresh = useCallback(async () => {
31
+ if (!isNative) {
32
+ setContacts([]);
33
+ setError(null);
34
+ setLoading(false);
35
+ return;
36
+ }
37
+ setLoading(true);
38
+ setError(null);
39
+ try {
40
+ const status = await Contacts.requestPermissions().catch(() => null);
41
+ if (status && status.contacts !== "granted") {
42
+ setContacts([]);
43
+ setError(
44
+ "Contacts access is needed to show your address book. Grant it in your device settings, then retry."
45
+ );
46
+ return;
47
+ }
48
+ const result = await Contacts.listContacts({});
49
+ setContacts(result.contacts);
50
+ } catch (err) {
51
+ setError(err instanceof Error ? err.message : String(err));
52
+ setContacts([]);
53
+ } finally {
54
+ setLoading(false);
55
+ }
56
+ }, []);
57
+ const autoLoadedRef = useRef(false);
58
+ useEffect(() => {
59
+ if (!autoLoadedRef.current) {
60
+ autoLoadedRef.current = true;
61
+ void refresh();
62
+ }
63
+ const interval = setInterval(() => {
64
+ void refresh();
65
+ }, 2e4);
66
+ return () => clearInterval(interval);
67
+ }, [refresh]);
68
+ const visibleContacts = useMemo(() => {
69
+ const needle = query.trim();
70
+ if (!needle) return contacts;
71
+ return contacts.filter((contact) => matchesQuery(contact, needle));
72
+ }, [contacts, query]);
73
+ const createContact = useCallback(async () => {
74
+ const displayName = form.displayName.trim();
75
+ if (!displayName || submitting) return;
76
+ const payload = { displayName };
77
+ const phone = form.phoneNumber.trim();
78
+ const email = form.emailAddress.trim();
79
+ if (phone) payload.phoneNumber = phone;
80
+ if (email) payload.emailAddress = email;
81
+ setSubmitting(true);
82
+ setError(null);
83
+ try {
84
+ await Contacts.createContact(payload);
85
+ setForm(EMPTY_FORM);
86
+ setMode("list");
87
+ await refresh();
88
+ } catch (err) {
89
+ setError(err instanceof Error ? err.message : String(err));
90
+ } finally {
91
+ setSubmitting(false);
92
+ }
93
+ }, [form, refresh, submitting]);
94
+ const onAction = useCallback(
95
+ (action) => {
96
+ if (action.startsWith("select:")) {
97
+ setSelectedId(action.slice("select:".length));
98
+ setMode("detail");
99
+ return;
100
+ }
101
+ if (action.startsWith("call:")) {
102
+ navigateToPhoneWithNumber(action.slice("call:".length));
103
+ return;
104
+ }
105
+ if (action.startsWith("text:")) {
106
+ navigateToMessagesWithNumber(action.slice("text:".length));
107
+ return;
108
+ }
109
+ if (action.startsWith("search:")) {
110
+ setQuery(action.slice("search:".length));
111
+ return;
112
+ }
113
+ if (action.startsWith("name:")) {
114
+ const value = action.slice("name:".length);
115
+ setForm((prev) => ({ ...prev, displayName: value }));
116
+ return;
117
+ }
118
+ if (action.startsWith("phone:")) {
119
+ const value = action.slice("phone:".length);
120
+ setForm((prev) => ({ ...prev, phoneNumber: value }));
121
+ return;
122
+ }
123
+ if (action.startsWith("email:")) {
124
+ const value = action.slice("email:".length);
125
+ setForm((prev) => ({ ...prev, emailAddress: value }));
126
+ return;
127
+ }
128
+ switch (action) {
129
+ case "refresh":
130
+ void refresh();
131
+ return;
132
+ case "new":
133
+ setForm(EMPTY_FORM);
134
+ setMode("new");
135
+ return;
136
+ case "back":
137
+ case "cancel":
138
+ setMode("list");
139
+ setSelectedId(null);
140
+ return;
141
+ case "save":
142
+ void createContact();
143
+ return;
144
+ }
145
+ },
146
+ [createContact, refresh]
147
+ );
148
+ const snapshot = {
149
+ contacts: visibleContacts,
150
+ query,
151
+ mode,
152
+ selectedId,
153
+ form,
154
+ loading,
155
+ submitting,
156
+ error
157
+ };
158
+ return /* @__PURE__ */ jsx(SpatialSurface, { children: /* @__PURE__ */ jsx(ContactsSpatialView, { snapshot, onAction }) });
159
+ }
160
+ export {
161
+ ContactsView
162
+ };
163
+ //# sourceMappingURL=ContactsView.js.map