From eb7ff7ae8f8ea68aafbf6ae094c48665cc1f4572 Mon Sep 17 00:00:00 2001 From: Albrecht Degering Date: Wed, 2 Sep 2026 11:45:06 +0200 Subject: [PATCH] Release Contact Tools 0.2.0 --- .gitea/workflows/verify.yml | 39 ++ CHANGELOG.md | 7 + README.md | 11 +- SOURCE.md | 4 +- THIRD_PARTY_NOTICES.md | 8 +- docs/ARCHITECTURE.md | 2 +- package-lock.json | 45 +-- package.json | 10 +- playwright.config.ts | 22 +- public/CHANGELOG.md | 7 + public/LICENSES/npm-runtime-licenses.txt | 6 +- public/README.md | 11 +- public/SOURCE.md | 4 +- public/THIRD_PARTY_NOTICES.md | 8 +- public/docs/ARCHITECTURE.md | 2 +- public/sw.js | 2 +- public/toolbox-app.json | 15 +- src/components/Workbench.tsx | 256 +++++++++++- src/contact/interchange.ts | 495 +++++++++++++++++++++++ src/contact/model.ts | 372 +++++++++++++++-- src/styles.css | 2 +- src/toolbox/manifest.source.json | 36 +- src/version.ts | 2 +- tests/browser/app.spec.ts | 4 +- tests/browser/responsive.spec.ts | 18 + tests/components/app.test.tsx | 2 +- tests/contact/model.test.ts | 76 ++++ 27 files changed, 1356 insertions(+), 110 deletions(-) create mode 100644 .gitea/workflows/verify.yml create mode 100644 src/contact/interchange.ts create mode 100644 tests/browser/responsive.spec.ts diff --git a/.gitea/workflows/verify.yml b/.gitea/workflows/verify.yml new file mode 100644 index 0000000..b84a7cb --- /dev/null +++ b/.gitea/workflows/verify.yml @@ -0,0 +1,39 @@ +name: Verify + +on: + push: + branches: [main] + pull_request: + workflow_dispatch: + +concurrency: + group: verify-${{ gitea.repository }}-${{ gitea.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + verify: + runs-on: ubuntu-latest + timeout-minutes: 45 + env: + CI: "true" + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: "22" + cache: npm + - name: Select declared npm version + run: npm install --global npm@11.17.0 + - name: Install dependencies + run: npm ci + - name: Audit runtime dependencies + run: npm audit --omit=dev --audit-level=moderate + - name: Check, test, and build + run: npm run check + - name: Install browser engines + run: npx playwright install --with-deps chromium firefox webkit + - name: Browser tests + run: npm run test:browser diff --git a/CHANGELOG.md b/CHANGELOG.md index fb117e2..d226b92 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## 0.2.0 - 2026-09-02 + +- Added vCard 2.1, grouped-property, quoted-printable and Base64 support while + preserving unmapped properties and parameters. +- Added loss-preserving jCard and focused JSContact import/export with explicit + mapping evidence. + ## 0.1.0 - 2026-09-01 - Added the initial local-first Contact Tools workbench. diff --git a/README.md b/README.md index 43418bf..4f5cf56 100644 --- a/README.md +++ b/README.md @@ -6,15 +6,18 @@ Contact Tools is a standalone local-first application in the [add·ideas Toolbox ## Version 0.1 scope -- Unfolding and bounded parsing of vCard 3.0 and 4.0 contact sets +- Unfolding and bounded parsing of vCard 2.1, 3.0 and 4.0 contact sets - Editable core names, organizations, titles, emails, phones, addresses, URLs, birthdays, notes, UIDs and categories -- Canonical vCard 3.0/4.0 export with UTF-8-aware 75-octet line folding +- Canonical vCard 2.1/3.0/4.0 export with UTF-8-aware 75-octet line folding +- Quoted-printable and bounded Base64 decoding, grouped-property support, and loss-preserving retention of unmapped IANA/X properties and parameters +- Loss-preserving RFC 7095 jCard import/export and a focused JSContact + import/export adapter with unmapped-field evidence - RFC 4180-style CSV import with inferred, reviewable column mapping and spreadsheet-formula-safe CSV export - Explainable exact duplicate candidates and a conflict-reporting, left-biased merge preview - Explicit, local vCard QR generation with a conservative payload bound and SVG download - Masked overview fields, one-click memory clearing, and no persistence or telemetry -Text input is limited to 2 MiB, 50,000 physical lines and 2,000 contacts. Binary and quoted-printable properties are diagnosed but not decoded, and unsupported properties are reported rather than silently round-tripped. See [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) and [docs/PRIVACY-SECURITY.md](docs/PRIVACY-SECURITY.md). +Text input is limited to 2 MiB, 50,000 physical lines and 2,000 contacts. Encoded values are limited to 64 KiB per property; malformed encodings are diagnosed while their raw values remain preserved. See [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) and [docs/PRIVACY-SECURITY.md](docs/PRIVACY-SECURITY.md). ## Development @@ -28,7 +31,7 @@ npm run test:browser ## Release -`npm run release:artifact` creates a deterministic `release/contact-tools-0.1.0.zip` and checksum sidecar. +`npm run release:artifact` creates a deterministic `release/contact-tools-0.2.0.zip` and checksum sidecar. ## Licence diff --git a/SOURCE.md b/SOURCE.md index fe99d41..b6555f8 100644 --- a/SOURCE.md +++ b/SOURCE.md @@ -1,7 +1,7 @@ # Corresponding source -The corresponding source for Contact Tools 0.1.0 is available at: +The corresponding source for Contact Tools 0.2.0 is available at: -https://git.add-ideas.de/lotobo/contact-tools/src/tag/v0.1.0 +https://git.add-ideas.de/lotobo/contact-tools/src/tag/v0.2.0 Build with Node.js 22, npm 11, `npm ci`, and `npm run release:artifact`. diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md index 8f761be..19a793a 100644 --- a/THIRD_PARTY_NOTICES.md +++ b/THIRD_PARTY_NOTICES.md @@ -1,12 +1,12 @@ # Third-party notices -Contact Tools 0.1.0 directly depends on these runtime packages: +Contact Tools 0.2.0 directly depends on these runtime packages: | Package | Pinned version | Declared licence | | -------------------------------- | -------------: | ---------------- | -| `@add-ideas/toolbox-contract` | 0.2.3 | Apache-2.0 | -| `@add-ideas/toolbox-helpers` | 0.1.0 | GPL-3.0-or-later | -| `@add-ideas/toolbox-shell-react` | 0.2.3 | Apache-2.0 | +| `@add-ideas/toolbox-contract` | 0.3.0 | Apache-2.0 | +| `@add-ideas/toolbox-helpers` | 0.2.0 | GPL-3.0-or-later | +| `@add-ideas/toolbox-shell-react` | 0.3.0 | Apache-2.0 | | `qrcode-generator` | 2.0.4 | MIT | | `react` | 19.2.8 | MIT | | `react-dom` | 19.2.8 | MIT | diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index e4f0273..96c3074 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -2,7 +2,7 @@ Contact Tools is a static React/Vite application wrapped in the shared Toolbox shell. `contact/model.ts` owns bounded vCard unfolding/parsing, canonical serialization, RFC 4180-style CSV conversion, exact duplicate indexing and deterministic merge previews. The workbench only replaces its active records after a complete successful parse. -Imported properties are projected into a typed contact model and rendered through React text nodes and native controls. Unknown properties are diagnosed and omitted from canonical output instead of being injected back into markup. QR SVG is generated from UTF-8 bytes of the canonical vCard by the pinned `qrcode-generator` runtime, checked for active SVG constructs, then displayed as an encoded image URL rather than inserted as markup. +Imported properties are projected into a typed contact model and rendered through React text nodes and native controls. Quoted-printable and bounded Base64 values are decoded locally. Unmapped properties retain their group, name, parameters, raw value and optional decoded bytes, are diagnosed as non-editable, and are serialized back as inert vCard content. QR SVG is generated from UTF-8 bytes of the canonical vCard by the pinned `qrcode-generator` runtime, checked for active SVG constructs, then displayed as an encoded image URL rather than inserted as markup. Version 0.1 operations are synchronous and bounded, so the app creates no worker and has no persistence or server API. Relative entry and asset URLs keep the build relocatable below a nested portal path. diff --git a/package-lock.json b/package-lock.json index bbd7b8a..e7271af 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,23 +1,23 @@ { "name": "contact-tools", - "version": "0.1.0", + "version": "0.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "contact-tools", - "version": "0.1.0", + "version": "0.2.0", "license": "GPL-3.0-or-later", "dependencies": { - "@add-ideas/toolbox-contract": "0.2.3", - "@add-ideas/toolbox-helpers": "0.1.0", - "@add-ideas/toolbox-shell-react": "0.2.3", + "@add-ideas/toolbox-contract": "0.3.0", + "@add-ideas/toolbox-helpers": "0.2.0", + "@add-ideas/toolbox-shell-react": "0.3.0", "qrcode-generator": "2.0.4", "react": "19.2.8", "react-dom": "19.2.8" }, "devDependencies": { - "@add-ideas/toolbox-testkit": "0.2.3", + "@add-ideas/toolbox-testkit": "0.3.0", "@eslint/js": "10.0.1", "@playwright/test": "1.62.1", "@testing-library/jest-dom": "6.9.1", @@ -43,24 +43,24 @@ } }, "node_modules/@add-ideas/toolbox-contract": { - "version": "0.2.3", - "license": "Apache-2.0", - "engines": { - "node": ">=20" - } + "version": "0.3.0", + "resolved": "https://git.add-ideas.de/api/packages/lotobo/npm/%40add-ideas%2Ftoolbox-contract/-/0.3.0/toolbox-contract-0.3.0.tgz", + "integrity": "sha512-dKrK7BjOFwqJaBfJuhKxZKIld4sH0AKjEn6a0yLnbdMUFY+fFv4VSLGV2tNSBD016gumc2iNqOjUj/ld7x4rtA==", + "license": "Apache-2.0" }, "node_modules/@add-ideas/toolbox-helpers": { - "version": "0.1.0", - "license": "GPL-3.0-or-later", - "engines": { - "node": ">=22" - } + "version": "0.2.0", + "resolved": "https://git.add-ideas.de/api/packages/lotobo/npm/%40add-ideas%2Ftoolbox-helpers/-/0.2.0/toolbox-helpers-0.2.0.tgz", + "integrity": "sha512-SdOqkw+P+3J3fa5iVkzb5P15rVepB001GNV21Oh8w0CZcVL+YRltgD/s+MVcTyrNijWQf3E5vtQON/3N2LLyKg==", + "license": "GPL-3.0-or-later" }, "node_modules/@add-ideas/toolbox-shell-react": { - "version": "0.2.3", + "version": "0.3.0", + "resolved": "https://git.add-ideas.de/api/packages/lotobo/npm/%40add-ideas%2Ftoolbox-shell-react/-/0.3.0/toolbox-shell-react-0.3.0.tgz", + "integrity": "sha512-74p6JzAOG0YCAKdlc1hLofV4ZIko7vb448S75cIiM88PKm93EHl5VD7g8YVyfM56Ui97UY9dmy+Whiq4sGzpsg==", "license": "Apache-2.0", "dependencies": { - "@add-ideas/toolbox-contract": "0.2.3" + "@add-ideas/toolbox-contract": "0.3.0" }, "peerDependencies": { "react": ">=18 <20", @@ -68,17 +68,16 @@ } }, "node_modules/@add-ideas/toolbox-testkit": { - "version": "0.2.3", + "version": "0.3.0", + "resolved": "https://git.add-ideas.de/api/packages/lotobo/npm/%40add-ideas%2Ftoolbox-testkit/-/0.3.0/toolbox-testkit-0.3.0.tgz", + "integrity": "sha512-4Fk+oSvZFspOMIXr8Xy040nhAaBsIQAzsGyXWSpjn3+k3yBKq7nB1r5zCHhsXzfdLzvPDAx2KcmSNOhM330D9w==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@add-ideas/toolbox-contract": "0.2.3" + "@add-ideas/toolbox-contract": "0.3.0" }, "bin": { "toolbox-check": "dist/cli.js" - }, - "engines": { - "node": ">=20" } }, "node_modules/@adobe/css-tools": { diff --git a/package.json b/package.json index 76d6606..a64ffc1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "contact-tools", - "version": "0.1.0", + "version": "0.2.0", "description": "Inspect, convert, and reconcile contacts locally in the browser.", "license": "GPL-3.0-or-later", "author": "Albrecht Degering", @@ -39,15 +39,15 @@ "release:artifact": "npm run check && npm run test:browser && npm run package:release -- --force" }, "dependencies": { - "@add-ideas/toolbox-contract": "0.2.3", - "@add-ideas/toolbox-helpers": "0.1.0", - "@add-ideas/toolbox-shell-react": "0.2.3", + "@add-ideas/toolbox-contract": "0.3.0", + "@add-ideas/toolbox-helpers": "0.2.0", + "@add-ideas/toolbox-shell-react": "0.3.0", "qrcode-generator": "2.0.4", "react": "19.2.8", "react-dom": "19.2.8" }, "devDependencies": { - "@add-ideas/toolbox-testkit": "0.2.3", + "@add-ideas/toolbox-testkit": "0.3.0", "@eslint/js": "10.0.1", "@playwright/test": "1.62.1", "@testing-library/jest-dom": "6.9.1", diff --git a/playwright.config.ts b/playwright.config.ts index 731da62..2ab6a22 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -15,7 +15,25 @@ export default defineConfig({ timeout: 180_000, }, projects: [ - { name: "chromium", use: { ...devices["Desktop Chrome"] } }, - { name: "firefox", use: { ...devices["Desktop Firefox"] } }, + { + name: "chromium", + testIgnore: /responsive\.spec\.ts/, + use: { ...devices["Desktop Chrome"] }, + }, + { + name: "firefox", + testIgnore: /responsive\.spec\.ts/, + use: { ...devices["Desktop Firefox"] }, + }, + { + name: "webkit", + testIgnore: /responsive\.spec\.ts/, + use: { ...devices["Desktop Safari"] }, + }, + { + name: "mobile-chromium", + testMatch: /responsive\.spec\.ts/, + use: { ...devices["Pixel 5"] }, + }, ], }); diff --git a/public/CHANGELOG.md b/public/CHANGELOG.md index fb117e2..d226b92 100644 --- a/public/CHANGELOG.md +++ b/public/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## 0.2.0 - 2026-09-02 + +- Added vCard 2.1, grouped-property, quoted-printable and Base64 support while + preserving unmapped properties and parameters. +- Added loss-preserving jCard and focused JSContact import/export with explicit + mapping evidence. + ## 0.1.0 - 2026-09-01 - Added the initial local-first Contact Tools workbench. diff --git a/public/LICENSES/npm-runtime-licenses.txt b/public/LICENSES/npm-runtime-licenses.txt index 1d98299..641f8db 100644 --- a/public/LICENSES/npm-runtime-licenses.txt +++ b/public/LICENSES/npm-runtime-licenses.txt @@ -1,5 +1,5 @@ ============================================================================== -@add-ideas/toolbox-contract@0.2.3 +@add-ideas/toolbox-contract@0.3.0 Declared licence: Apache-2.0 ============================================================================== --- LICENSE --- @@ -198,7 +198,7 @@ Declared licence: Apache-2.0 ============================================================================== -@add-ideas/toolbox-helpers@0.1.0 +@add-ideas/toolbox-helpers@0.2.0 Declared licence: GPL-3.0-or-later ============================================================================== --- LICENSE --- @@ -879,7 +879,7 @@ Public License instead of this License. But first, please read ============================================================================== -@add-ideas/toolbox-shell-react@0.2.3 +@add-ideas/toolbox-shell-react@0.3.0 Declared licence: Apache-2.0 ============================================================================== --- LICENSE --- diff --git a/public/README.md b/public/README.md index 43418bf..4f5cf56 100644 --- a/public/README.md +++ b/public/README.md @@ -6,15 +6,18 @@ Contact Tools is a standalone local-first application in the [add·ideas Toolbox ## Version 0.1 scope -- Unfolding and bounded parsing of vCard 3.0 and 4.0 contact sets +- Unfolding and bounded parsing of vCard 2.1, 3.0 and 4.0 contact sets - Editable core names, organizations, titles, emails, phones, addresses, URLs, birthdays, notes, UIDs and categories -- Canonical vCard 3.0/4.0 export with UTF-8-aware 75-octet line folding +- Canonical vCard 2.1/3.0/4.0 export with UTF-8-aware 75-octet line folding +- Quoted-printable and bounded Base64 decoding, grouped-property support, and loss-preserving retention of unmapped IANA/X properties and parameters +- Loss-preserving RFC 7095 jCard import/export and a focused JSContact + import/export adapter with unmapped-field evidence - RFC 4180-style CSV import with inferred, reviewable column mapping and spreadsheet-formula-safe CSV export - Explainable exact duplicate candidates and a conflict-reporting, left-biased merge preview - Explicit, local vCard QR generation with a conservative payload bound and SVG download - Masked overview fields, one-click memory clearing, and no persistence or telemetry -Text input is limited to 2 MiB, 50,000 physical lines and 2,000 contacts. Binary and quoted-printable properties are diagnosed but not decoded, and unsupported properties are reported rather than silently round-tripped. See [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) and [docs/PRIVACY-SECURITY.md](docs/PRIVACY-SECURITY.md). +Text input is limited to 2 MiB, 50,000 physical lines and 2,000 contacts. Encoded values are limited to 64 KiB per property; malformed encodings are diagnosed while their raw values remain preserved. See [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) and [docs/PRIVACY-SECURITY.md](docs/PRIVACY-SECURITY.md). ## Development @@ -28,7 +31,7 @@ npm run test:browser ## Release -`npm run release:artifact` creates a deterministic `release/contact-tools-0.1.0.zip` and checksum sidecar. +`npm run release:artifact` creates a deterministic `release/contact-tools-0.2.0.zip` and checksum sidecar. ## Licence diff --git a/public/SOURCE.md b/public/SOURCE.md index fe99d41..b6555f8 100644 --- a/public/SOURCE.md +++ b/public/SOURCE.md @@ -1,7 +1,7 @@ # Corresponding source -The corresponding source for Contact Tools 0.1.0 is available at: +The corresponding source for Contact Tools 0.2.0 is available at: -https://git.add-ideas.de/lotobo/contact-tools/src/tag/v0.1.0 +https://git.add-ideas.de/lotobo/contact-tools/src/tag/v0.2.0 Build with Node.js 22, npm 11, `npm ci`, and `npm run release:artifact`. diff --git a/public/THIRD_PARTY_NOTICES.md b/public/THIRD_PARTY_NOTICES.md index 8f761be..19a793a 100644 --- a/public/THIRD_PARTY_NOTICES.md +++ b/public/THIRD_PARTY_NOTICES.md @@ -1,12 +1,12 @@ # Third-party notices -Contact Tools 0.1.0 directly depends on these runtime packages: +Contact Tools 0.2.0 directly depends on these runtime packages: | Package | Pinned version | Declared licence | | -------------------------------- | -------------: | ---------------- | -| `@add-ideas/toolbox-contract` | 0.2.3 | Apache-2.0 | -| `@add-ideas/toolbox-helpers` | 0.1.0 | GPL-3.0-or-later | -| `@add-ideas/toolbox-shell-react` | 0.2.3 | Apache-2.0 | +| `@add-ideas/toolbox-contract` | 0.3.0 | Apache-2.0 | +| `@add-ideas/toolbox-helpers` | 0.2.0 | GPL-3.0-or-later | +| `@add-ideas/toolbox-shell-react` | 0.3.0 | Apache-2.0 | | `qrcode-generator` | 2.0.4 | MIT | | `react` | 19.2.8 | MIT | | `react-dom` | 19.2.8 | MIT | diff --git a/public/docs/ARCHITECTURE.md b/public/docs/ARCHITECTURE.md index e4f0273..96c3074 100644 --- a/public/docs/ARCHITECTURE.md +++ b/public/docs/ARCHITECTURE.md @@ -2,7 +2,7 @@ Contact Tools is a static React/Vite application wrapped in the shared Toolbox shell. `contact/model.ts` owns bounded vCard unfolding/parsing, canonical serialization, RFC 4180-style CSV conversion, exact duplicate indexing and deterministic merge previews. The workbench only replaces its active records after a complete successful parse. -Imported properties are projected into a typed contact model and rendered through React text nodes and native controls. Unknown properties are diagnosed and omitted from canonical output instead of being injected back into markup. QR SVG is generated from UTF-8 bytes of the canonical vCard by the pinned `qrcode-generator` runtime, checked for active SVG constructs, then displayed as an encoded image URL rather than inserted as markup. +Imported properties are projected into a typed contact model and rendered through React text nodes and native controls. Quoted-printable and bounded Base64 values are decoded locally. Unmapped properties retain their group, name, parameters, raw value and optional decoded bytes, are diagnosed as non-editable, and are serialized back as inert vCard content. QR SVG is generated from UTF-8 bytes of the canonical vCard by the pinned `qrcode-generator` runtime, checked for active SVG constructs, then displayed as an encoded image URL rather than inserted as markup. Version 0.1 operations are synchronous and bounded, so the app creates no worker and has no persistence or server API. Relative entry and asset URLs keep the build relocatable below a nested portal path. diff --git a/public/sw.js b/public/sw.js index b4724a2..268e602 100644 --- a/public/sw.js +++ b/public/sw.js @@ -1,5 +1,5 @@ const CACHE_PREFIX = "contact-tools-shell-"; -const CACHE_NAME = CACHE_PREFIX + "0.1.0"; +const CACHE_NAME = CACHE_PREFIX + "0.2.0"; const CORE = ["./", "./manifest.webmanifest", "./favicon.svg"]; self.addEventListener("install", (event) => { event.waitUntil( diff --git a/public/toolbox-app.json b/public/toolbox-app.json index 571c0ce..757d3f0 100644 --- a/public/toolbox-app.json +++ b/public/toolbox-app.json @@ -3,7 +3,7 @@ "schemaVersion": 1, "id": "de.add-ideas.contact-tools", "name": "Contact Tools", - "version": "0.1.0", + "version": "0.2.0", "description": "Inspect, convert, and reconcile contacts locally in the browser.", "entry": "./", "icon": "./favicon.svg", @@ -21,6 +21,19 @@ "crossOriginIsolated": false, "topLevelContext": false }, + "io": { + "accepts": [ + { "mediaType": "text/vcard", "extensions": [".vcf", ".vcard"] }, + { "mediaType": "application/vcard+json", "extensions": [".json"] }, + { "mediaType": "application/jscontact+json", "extensions": [".json"] } + ], + "produces": [ + { "mediaType": "text/vcard", "extensions": [".vcf"] }, + { "mediaType": "application/vcard+json", "extensions": [".json"] }, + { "mediaType": "application/jscontact+json", "extensions": [".json"] } + ] + }, + "capabilities": { "required": [], "optional": [] }, "privacy": { "processing": "local", "fileUploads": true, diff --git a/src/components/Workbench.tsx b/src/components/Workbench.tsx index 39157cd..eef4aa9 100644 --- a/src/components/Workbench.tsx +++ b/src/components/Workbench.tsx @@ -1,5 +1,14 @@ import { useMemo, useState } from "react"; -import { triggerBlobDownload } from "@add-ideas/toolbox-helpers"; +import { + stableStringify, + triggerBlobDownload, +} from "@add-ideas/toolbox-helpers"; +import { + contactsFromJCard, + contactsFromJSContact, + contactsToJCard, + contactsToJSContact, +} from "../contact/interchange"; import { CONTACT_FIELDS, contactsFromCsv, @@ -20,7 +29,7 @@ const sample = `BEGIN:VCARD\r\nVERSION:4.0\r\nFN:Ada Lovelace\r\nN:Lovelace;Ada; const sampleCsv = `Full Name,First Name,Last Name,Company,Email,Phone,City,Country\r\nGrace Hopper,Grace,Hopper,US Navy,grace@example.test,+1 555 0100,Arlington,United States\r\n`; -type Tab = "contacts" | "csv" | "duplicates" | "qr"; +type Tab = "contacts" | "csv" | "json" | "duplicates" | "qr"; const FIELD_LABELS: Record = { formattedName: "Full name", givenName: "Given name", @@ -64,7 +73,17 @@ function ContactEditor({ }) { const set = (field: keyof Contact, value: string) => onChange({ ...contact, [field]: value }); - const setPrimary = (kind: "emails" | "phones" | "urls", value: string) => { + const setPrimary = ( + kind: + | "emails" + | "phones" + | "urls" + | "languages" + | "timezones" + | "geos" + | "instantMessaging", + value: string, + ) => { const current = contact[kind]; const next = current.length ? current.map((item, index) => (index ? item : { ...item, value })) @@ -110,6 +129,42 @@ function ContactEditor({ onChange={(event) => set("title", event.target.value)} /> + + + + + + + + + + {contact.media.length ? ( +

+ Preserved media: {contact.media.map((item) => item.kind).join(", ")}. + Binary and URI values remain unchanged in canonical vCard/jCard + output. +

+ ) : null}