@hasna/contacts 0.9.0 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -34,7 +34,7 @@ page badge / popup ──► background service worker ──► native host (co
34
34
  - **Detection.** The popup shows the page it was opened on and how many supported fields it found. On every https page (and loopback test pages) a content script applies the same rules — `autocomplete` tokens first (`name`, `given-name`, `family-name`, `organization`, `organization-title`, `email`, `tel`, `url`, `bday`, `street-address`, `address-line1/2`, `address-level1/2`, `postal-code`, `country`, …), then common name/id/placeholder/label aliases (`first_name`, `company`, `phone`, `zip`, …), then `type=email|tel|url`. An explicit unrelated `autocomplete` token is authoritative. Password, hidden, disabled, read-only and `aria-hidden` controls are never targets. When the form has at least one identity field the content script draws a Contacts badge inside the name, email and first address field.
35
35
  - **Picking.** Clicking the badge (or opening the popup) shows a search box and a list of contacts: display name, company and primary email only. Search is bounded to 200 characters and 20 results. Choosing a contact asks the worker to fill **this tab**; a page cannot redirect the fill — the tab, frame, document and origin come from the sender Chrome attaches to the message, never from the message body.
36
36
  - **Filling.** The worker inspects the selected documents, then asks the native host to `resolve` that one contact into an `autocomplete`-keyed value map (primary email/phone/address first; country mapped to a two-letter code when the stored name is recognisable; birthday as `YYYY-MM-DD`). Values are injected only into exact document IDs that still match, through the native value setter with `input`/`change` events, and are deleted afterwards. Ambiguous duplicate fields, incompatible select options and controls that shrink below the value are skipped and reported in the receipt.
37
- - **Boundaries.** The content script never receives contact values, never reads field values, and ignores synthetic events — every action requires a trusted user click (`event.isTrusted`). The picker lives in a closed shadow root. The native host answers only the pinned extension id, has no verb that lists phones, addresses or notes, and never logs values. The worker accepts popup messages only from its own `popup.html` and page messages only from its own content scripts.
37
+ - **Boundaries.** The content script never receives contact values, never reads field values, and ignores synthetic events — every action requires a trusted user click (`event.isTrusted`). The picker lives in a closed shadow root inside a zero-size overlay (so it never counts as an occluder for another extension's controls). The native host answers only the pinned extension id, has no verb that lists phones, addresses or notes, and never logs values. The worker accepts popup messages only from its own `popup.html` and page messages only from its own content scripts.
38
38
 
39
39
  The picker defends against deceived clicks, not just synthetic ones. The badge host element is pinned with `!important` declarations (opacity, visibility, transform, filter, clip, blend, pointer events) so page CSS cannot hide, fade or move it; the layer sits at the maximum z-index; and every badge and contact row is watched with IntersectionObserver v2 (`trackVisibility`). A click counts only when it is trusted **and** the control has been continuously unoccluded and visually unaltered for 300 ms — a badge under an overlay, behind a transparent decoy or inside a transformed ancestor is refused, and a browser without IntersectionObserver v2 gets no in-page fills at all. The badge and picker live in a closed shadow root, are re-attached if the page removes them, rescan only on mutations that add or change form controls (bounded shadow-root walk), and disappear when no identity field is present. The receipt the page's picker receives is counts only (filled, skipped): no ids, origins or frames.
40
40
 
@@ -64,35 +64,40 @@
64
64
  // over page rules of any specificity or importance.
65
65
  const css = `
66
66
  :host { all: initial !important; display: block !important; position: fixed !important; top: 0 !important; left: 0 !important; width: 0 !important; height: 0 !important; margin: 0 !important; padding: 0 !important; border: 0 !important; overflow: visible !important; opacity: 1 !important; visibility: visible !important; transform: none !important; filter: none !important; backdrop-filter: none !important; clip-path: none !important; clip: auto !important; mask: none !important; mix-blend-mode: normal !important; pointer-events: auto !important; contain: none !important; content-visibility: visible !important; isolation: auto !important; perspective: none !important; zoom: 1 !important; translate: none !important; rotate: none !important; scale: none !important; }
67
- .layer { position: fixed; inset: 0; pointer-events: none; z-index: 2147483647; font: 13px/1.45 system-ui, -apple-system, "Segoe UI", sans-serif; color: #172033; }
68
- .badge { position: absolute; width: ${BADGE}px; height: ${BADGE}px; border: 0; border-radius: 6px; padding: 0; margin: 0; cursor: pointer; pointer-events: auto; background: #2e4a7a url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'><circle cx='10' cy='7.5' r='3.2' fill='%23ffffff'/><path d='M4.5 16c.5-3.3 2.8-5 5.5-5s5 1.7 5.5 5z' fill='%23d6e1f5'/></svg>") center/16px no-repeat; box-shadow: 0 1px 2px rgba(0,0,0,.25); }
69
- .badge:hover, .badge:focus-visible { box-shadow: 0 0 0 2px #8fb0e6, 0 1px 2px rgba(0,0,0,.25); outline: none; }
70
- .menu { position: absolute; min-width: 284px; max-width: 340px; pointer-events: auto; background: #ffffff; color: #172033; border: 1px solid #d8dfe9; border-radius: 12px; box-shadow: 0 12px 32px rgba(23, 44, 79, .18), 0 2px 6px rgba(0,0,0,.08); overflow: hidden; }
71
- .menu header { display: flex; align-items: center; gap: 8px; padding: 10px 12px 8px; border-bottom: 1px solid #e6ebf2; }
72
- .menu header .mark { width: 18px; height: 18px; border-radius: 5px; background: #2e4a7a; flex: none; }
73
- .menu header strong { font-size: 13px; font-weight: 650; }
74
- .menu header span { margin-left: auto; color: #6b7686; font-size: 11px; }
75
- .search { display: block; width: 100%; box-sizing: border-box; margin: 0; padding: 9px 12px; border: 0; border-bottom: 1px solid #e6ebf2; background: #f6f8fb; color: inherit; font: inherit; outline: none; }
76
- .search:focus { background: #eef3fa; }
77
- .list { list-style: none; margin: 0; padding: 6px; max-height: 240px; overflow: auto; }
78
- .item { display: flex; align-items: center; gap: 10px; width: 100%; padding: 7px 8px; border: 0; border-radius: 8px; background: transparent; color: inherit; font: inherit; text-align: left; cursor: pointer; }
79
- .item:hover, .item:focus-visible { background: #eef3fa; outline: none; }
80
- .item .avatar { width: 28px; height: 28px; border-radius: 50%; background: linear-gradient(135deg, #2e4a7a, #172c4f); color: #fff; font: 600 11px system-ui, sans-serif; display: grid; place-items: center; flex: none; }
67
+ .layer { position: fixed; top: 0; left: 0; width: 0; height: 0; overflow: visible; pointer-events: none; z-index: 2147483647; font: 13px/1.45 system-ui, -apple-system, "Segoe UI", "Helvetica Neue", sans-serif; color: #0a0a0a; --mono: ui-monospace, "SF Mono", SFMono-Regular, Menlo, Consolas, monospace; }
68
+ .badge { position: fixed; width: ${BADGE}px; height: ${BADGE}px; border: 0; border-radius: 4px; padding: 0; margin: 0; cursor: pointer; pointer-events: auto; background: #0a0a0a url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' fill='none' stroke='%23ffffff' stroke-width='1.6'><circle cx='10' cy='7.5' r='3.2'/><path d='M4.5 16.5c.6-3.6 2.9-5.5 5.5-5.5s4.9 1.9 5.5 5.5'/></svg>") center/14px no-repeat; box-shadow: 0 0 0 1px rgba(255,255,255,.9); }
69
+ .badge:hover, .badge:focus-visible { box-shadow: 0 0 0 1px rgba(255,255,255,.9), 0 0 0 3px #0a0a0a; outline: none; }
70
+ .menu { position: fixed; min-width: 284px; max-width: 340px; pointer-events: auto; background: #ffffff; color: #0a0a0a; border: 1px solid #0a0a0a; border-radius: 4px; box-shadow: 0 16px 40px rgba(0,0,0,.16); overflow: hidden; }
71
+ .menu header { display: flex; align-items: center; gap: 8px; padding: 10px 12px; border-bottom: 1px solid #0a0a0a; }
72
+ .menu header .mark { width: 8px; height: 8px; background: #0a0a0a; flex: none; }
73
+ .menu header strong { font: 10px var(--mono); letter-spacing: .18em; text-transform: uppercase; }
74
+ .menu header span { margin-left: auto; font: 10px var(--mono); letter-spacing: .04em; color: #6f6f6f; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 55%; }
75
+ .search { display: block; width: 100%; box-sizing: border-box; margin: 0; padding: 9px 12px; border: 0; border-bottom: 1px solid #e4e4e4; background: #ffffff; color: inherit; font: 13px system-ui, -apple-system, "Segoe UI", sans-serif; outline: none; }
76
+ .search::placeholder { color: #8c8c8c; }
77
+ .search:focus { border-bottom-color: #0a0a0a; }
78
+ .list { list-style: none; margin: 0; padding: 0; max-height: 240px; overflow: auto; }
79
+ .item { display: flex; align-items: center; gap: 12px; width: 100%; padding: 9px 12px; border: 0; border-bottom: 1px solid #e4e4e4; border-radius: 0; background: transparent; color: inherit; font: inherit; text-align: left; cursor: pointer; transition: background .1s ease, color .1s ease; }
80
+ .item:last-child { border-bottom: 0; }
81
+ .item:hover, .item:focus-visible { background: #0a0a0a; color: #ffffff; outline: none; }
82
+ .item .avatar { width: 26px; height: 26px; border: 1px solid currentColor; border-radius: 3px; display: grid; place-items: center; font: 10px var(--mono); letter-spacing: .06em; flex: none; }
81
83
  .item .meta { flex: 1; min-width: 0; }
82
- .item .name { display: block; font-weight: 550; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
83
- .item .sub { display: block; color: #6b7686; font-size: 11px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
84
- .note { padding: 10px 12px; color: #55606f; font-size: 12px; }
85
- .note.error { color: #9b3229; }
86
- .note code { font: 11px ui-monospace, SFMono-Regular, Menlo, monospace; background: #f1f4f8; padding: 1px 4px; border-radius: 4px; }
87
- .foot { padding: 6px 12px 9px; color: #7a8492; font-size: 11px; border-top: 1px solid #e6ebf2; }
84
+ .item .name { display: block; font-weight: 500; letter-spacing: -.01em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
85
+ .item .sub { display: block; font: 10px var(--mono); letter-spacing: .04em; opacity: .6; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
86
+ .note { padding: 10px 12px; color: #6f6f6f; font: 11px var(--mono); letter-spacing: .02em; }
87
+ .note.error { color: #0a0a0a; }
88
+ .note code { font: 11px var(--mono); border: 1px solid #e4e4e4; padding: 1px 4px; border-radius: 3px; }
89
+ .foot { padding: 7px 12px 8px; color: #8c8c8c; font: 10px var(--mono); letter-spacing: .06em; text-transform: uppercase; border-top: 1px solid #e4e4e4; }
88
90
  @media (prefers-color-scheme: dark) {
89
- .layer { color: #e6ebf3; }
90
- .menu { background: #171e2b; color: #e6ebf3; border-color: #2b3546; box-shadow: 0 12px 32px rgba(0,0,0,.5); }
91
- .menu header, .foot { border-color: #253046; }
92
- .search { background: #1d2636; border-color: #253046; } .search:focus { background: #223047; }
93
- .item:hover, .item:focus-visible { background: #223047; }
94
- .note { color: #aab4c3; } .note code { background: #223047; }
95
- .foot, .menu header span, .item .sub { color: #8d98a8; }
91
+ .layer { color: #f4f4f4; }
92
+ .badge { background-color: #f4f4f4; background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' fill='none' stroke='%230a0a0a' stroke-width='1.6'><circle cx='10' cy='7.5' r='3.2'/><path d='M4.5 16.5c.6-3.6 2.9-5.5 5.5-5.5s4.9 1.9 5.5 5.5'/></svg>"); box-shadow: 0 0 0 1px rgba(0,0,0,.9); }
93
+ .badge:hover, .badge:focus-visible { box-shadow: 0 0 0 1px rgba(0,0,0,.9), 0 0 0 3px #f4f4f4; }
94
+ .menu { background: #0a0a0a; color: #f4f4f4; border-color: #f4f4f4; box-shadow: 0 16px 40px rgba(0,0,0,.6); }
95
+ .menu header { border-color: #f4f4f4; } .menu header .mark { background: #f4f4f4; } .menu header span { color: #8c8c8c; }
96
+ .search { background: #0a0a0a; border-color: #262626; } .search:focus { border-bottom-color: #f4f4f4; }
97
+ .item { border-color: #262626; }
98
+ .item:hover, .item:focus-visible { background: #f4f4f4; color: #0a0a0a; }
99
+ .note { color: #8c8c8c; } .note.error { color: #f4f4f4; } .note code { border-color: #262626; }
100
+ .foot { border-color: #262626; }
96
101
  }`;
97
102
 
98
103
  function parent() {
@@ -275,7 +280,7 @@
275
280
  const mark = document.createElement("span");
276
281
  mark.className = "mark";
277
282
  const title = document.createElement("strong");
278
- title.textContent = "Fill from Contacts";
283
+ title.textContent = "Contacts";
279
284
  const site = document.createElement("span");
280
285
  site.textContent = location.hostname;
281
286
  header.append(mark, title, site);
@@ -289,7 +294,7 @@
289
294
  body.append(note("Loading your contacts…"));
290
295
  const foot = document.createElement("div");
291
296
  foot.className = "foot";
292
- foot.textContent = "Fills name, email, phone, company and address fields.";
297
+ foot.textContent = "Name · email · phone · address · nothing submitted";
293
298
  menu.append(header, input, body, foot);
294
299
  state.layer.appendChild(menu);
295
300
  state.menu = menu;
Binary file
Binary file
Binary file
Binary file
@@ -1,17 +1,7 @@
1
1
  <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128">
2
- <defs>
3
- <linearGradient id="bg" x1="0" y1="0" x2="1" y2="1">
4
- <stop offset="0" stop-color="#2E4A7A"/>
5
- <stop offset="1" stop-color="#172C4F"/>
6
- </linearGradient>
7
- <linearGradient id="fg" x1="0" y1="0" x2="0" y2="1">
8
- <stop offset="0" stop-color="#FFFFFF"/>
9
- <stop offset="1" stop-color="#D6E1F5"/>
10
- </linearGradient>
11
- </defs>
12
- <rect x="4" y="4" width="120" height="120" rx="30" fill="url(#bg)"/>
13
- <circle cx="64" cy="50" r="19" fill="url(#fg)"/>
14
- <path d="M28 104c3-22 18-33 36-33s33 11 36 33c0 3-2 5-5 5H33c-3 0-5-2-5-5z" fill="url(#fg)"/>
15
- <circle cx="96" cy="36" r="11" fill="#E8B04B"/>
16
- <path d="M91 36l3.5 3.5L101.5 32" fill="none" stroke="#172C4F" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
2
+ <rect x="4" y="4" width="120" height="120" rx="28" fill="#0A0A0A"/>
3
+ <g fill="none" stroke="#FFFFFF" stroke-width="7" stroke-linecap="round" stroke-linejoin="round">
4
+ <circle cx="64" cy="50" r="17"/>
5
+ <path d="M32 100c4-20 17-30 32-30s28 10 32 30"/>
6
+ </g>
17
7
  </svg>
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "manifest_version": 3,
3
3
  "name": "Hasna Contacts",
4
- "version": "0.9.0",
4
+ "version": "0.9.1",
5
5
  "description": "Fill names, emails, phones, companies and addresses from your Contacts. Detects identity fields on the page and offers your contacts inline; nothing is filled until you pick one.",
6
6
  "minimum_chrome_version": "119",
7
7
  "key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzMCnB26YET/ZdPDrwxAnrgeekYVPjoWHUEFUHpiYHFtgjyuBsh63wTlw+nBn0730e84FbbWJ04WYzy9TQcaZK9haaGOALouQa9KTzufHC6JB1T7KdlnPGDgiIcUxsqXn18Rc3J8eWCYB4vX0wc0OgevLDjvbR917XwUpNFxv8jkVpe/yyyRvhG+MYFw9Mwy2j2FMqNHwmtlGXWEt9ZmcTUx0No2dCxbRGdZA9QzcpPgL2i2BTul1eQpLfc2kSbaBMtVrrwPXz0Ara0qjXQP2RLyHlj0t1f0W29AVm6mjRwVGxw/3M4kXfOsUpeEBSQxC8M3KMRd1sYgJ2GFNwmY/fwIDAQAB",
@@ -1,78 +1,71 @@
1
+ /* Contacts — monochrome system. One ink, one paper, hairlines, inverted hover. */
1
2
  :root {
2
3
  color-scheme: light dark;
3
- --bg: #f6f8fb;
4
- --surface: #ffffff;
5
- --fg: #172033;
6
- --muted: #66707f;
7
- --line: #e0e6ee;
8
- --brand: #2e4a7a;
9
- --brand-2: #172c4f;
10
- --brand-soft: #e9eff8;
11
- --accent: #8fb0e6;
12
- --danger: #9b3229;
13
- --shadow: 0 1px 2px rgba(0, 0, 0, 0.05), 0 8px 24px rgba(23, 44, 79, 0.06);
14
- --radius: 12px;
15
- font: 14px / 1.45 system-ui, -apple-system, "Segoe UI", sans-serif;
16
- color: var(--fg);
17
- background: var(--bg);
4
+ --paper: #ffffff;
5
+ --ink: #0a0a0a;
6
+ --muted: #6f6f6f;
7
+ --line: #e4e4e4;
8
+ --line-strong: #0a0a0a;
9
+ --mono: ui-monospace, "SF Mono", SFMono-Regular, Menlo, Consolas, monospace;
10
+ --sans: system-ui, -apple-system, "Segoe UI", "Helvetica Neue", sans-serif;
11
+ --r: 4px;
12
+ font: 13px / 1.45 var(--sans);
13
+ color: var(--ink);
14
+ background: var(--paper);
15
+ -webkit-font-smoothing: antialiased;
18
16
  }
19
17
  @media (prefers-color-scheme: dark) {
20
- :root {
21
- --bg: #12171f;
22
- --surface: #1a2130;
23
- --fg: #e6ebf3;
24
- --muted: #97a1b1;
25
- --line: #2a3446;
26
- --brand: #4a6ea8;
27
- --brand-2: #2e4a7a;
28
- --brand-soft: #1f2a3d;
29
- --accent: #6f93cc;
30
- --danger: #f08a80;
31
- --shadow: 0 1px 2px rgba(0, 0, 0, 0.4), 0 8px 24px rgba(0, 0, 0, 0.35);
32
- }
18
+ :root { --paper: #0a0a0a; --ink: #f4f4f4; --muted: #8c8c8c; --line: #262626; --line-strong: #f4f4f4; }
33
19
  }
34
20
  * { box-sizing: border-box; }
35
- body { margin: 0; width: 344px; min-height: 200px; display: flex; flex-direction: column; }
21
+ [hidden] { display: none !important; }
22
+ body { margin: 0; width: 336px; min-height: 180px; display: flex; flex-direction: column; background: var(--paper); }
36
23
  h1, h2, p { margin: 0; }
37
- code { font: 12px ui-monospace, SFMono-Regular, Menlo, monospace; background: var(--brand-soft); padding: 1px 5px; border-radius: 5px; }
24
+ code { font: 11px var(--mono); border: 1px solid var(--line); padding: 1px 5px; border-radius: var(--r); }
38
25
  .muted { color: var(--muted); }
39
- .small { font-size: 12px; }
26
+ .small { font-size: 11px; }
27
+ .label { font: 10px var(--mono); letter-spacing: 0.14em; text-transform: uppercase; color: var(--muted); }
40
28
 
41
- .top { display: flex; align-items: center; gap: 10px; padding: 14px 16px 12px; border-bottom: 1px solid var(--line); background: var(--surface); }
42
- .logo { border-radius: 8px; flex: none; }
29
+ .top { display: flex; align-items: center; gap: 10px; padding: 14px 14px 12px; border-bottom: 1px solid var(--line-strong); }
30
+ .mark { width: 22px; height: 22px; flex: none; background: var(--ink); color: var(--paper); border-radius: 5px; display: grid; place-items: center; }
31
+ .mark svg { width: 14px; height: 14px; display: block; }
43
32
  .brand { min-width: 0; flex: 1; }
44
- h1 { font-size: 15px; font-weight: 650; letter-spacing: -0.2px; line-height: 1.2; }
45
- .page { color: var(--muted); font-size: 12px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
46
- .page[data-ready="true"]::before { content: ""; display: inline-block; width: 7px; height: 7px; border-radius: 50%; background: #2f9e6a; margin-right: 6px; vertical-align: 1px; }
33
+ h1 { font: 11px var(--mono); letter-spacing: 0.18em; text-transform: uppercase; line-height: 1.2; }
34
+ .page { margin-top: 3px; font: 10.5px var(--mono); letter-spacing: 0.04em; color: var(--muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
35
+ .page[data-ready="true"] { color: var(--ink); }
36
+ .page[data-ready="true"]::before { content: ""; display: inline-block; width: 6px; height: 6px; background: var(--ink); margin-right: 7px; vertical-align: 1px; }
47
37
 
48
- main { padding: 12px 12px 4px; flex: 1; }
49
- .state { padding: 22px 10px; text-align: center; color: var(--muted); }
50
- .state h2 { color: var(--fg); font-size: 14px; font-weight: 650; margin-bottom: 6px; }
38
+ main { flex: 1; padding: 10px 14px 6px; }
39
+ .state { padding: 26px 8px 22px; text-align: center; color: var(--muted); }
40
+ .state h2 { color: var(--ink); font: 11px var(--mono); letter-spacing: 0.16em; text-transform: uppercase; margin-bottom: 8px; }
51
41
  .state p + p { margin-top: 8px; }
52
- .spinner { display: inline-block; width: 18px; height: 18px; border: 2px solid var(--line); border-top-color: var(--brand); border-radius: 50%; animation: spin 0.8s linear infinite; margin-bottom: 10px; }
42
+ .spinner { display: inline-block; width: 14px; height: 14px; border: 1px solid var(--line); border-top-color: var(--ink); border-radius: 50%; animation: spin 0.9s linear infinite; margin-bottom: 10px; }
53
43
  @keyframes spin { to { transform: rotate(360deg); } }
54
44
 
55
- .search { display: block; width: 100%; margin: 0 0 10px; padding: 9px 12px; border: 1px solid var(--line); border-radius: 10px; background: var(--surface); color: var(--fg); font: 14px system-ui, sans-serif; }
56
- .search:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
45
+ .search { display: block; width: 100%; margin: 0 0 10px; padding: 9px 0 9px 22px; border: 0; border-bottom: 1px solid var(--line-strong); border-radius: 0; background: transparent url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' fill='none' stroke='%236f6f6f' stroke-width='1.6'><circle cx='9' cy='9' r='5.5'/><path d='M13.5 13.5L17 17'/></svg>") left center/14px no-repeat; color: var(--ink); font: 13px var(--sans); }
46
+ .search::placeholder { color: var(--muted); }
47
+ .search:focus-visible { outline: none; border-bottom-width: 2px; }
48
+ .search::-webkit-search-cancel-button { -webkit-appearance: none; }
57
49
 
58
- .page-actions { display: flex; flex-direction: column; gap: 6px; padding: 0 4px 12px; }
59
- .chip-btn { align-self: flex-start; border: 1px solid var(--line); background: var(--surface); color: var(--brand); font: 600 12px system-ui, sans-serif; padding: 6px 10px; border-radius: 999px; cursor: pointer; }
60
- .chip-btn:hover, .chip-btn:focus-visible { border-color: var(--accent); outline: none; }
50
+ .page-actions { display: flex; flex-direction: column; gap: 6px; padding: 0 0 12px; }
51
+ .chip-btn { align-self: flex-start; border: 1px solid var(--line-strong); background: transparent; color: var(--ink); font: 10px var(--mono); letter-spacing: 0.12em; text-transform: uppercase; padding: 6px 10px; border-radius: var(--r); cursor: pointer; }
52
+ .chip-btn:hover, .chip-btn:focus-visible { background: var(--ink); color: var(--paper); outline: none; }
61
53
 
62
- .contacts { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 6px; max-height: 320px; overflow: auto; }
63
- .contact { display: flex; align-items: center; gap: 12px; width: 100%; padding: 9px 12px; border: 1px solid var(--line); border-radius: var(--radius); background: var(--surface); color: inherit; font: inherit; text-align: left; cursor: pointer; box-shadow: var(--shadow); transition: transform 0.12s ease, border-color 0.12s ease; }
64
- .contact:hover:not(:disabled), .contact:focus-visible { border-color: var(--accent); transform: translateY(-1px); outline: none; }
65
- .contact:disabled { cursor: default; opacity: 0.72; box-shadow: none; }
66
- .contact .avatar { flex: none; width: 34px; height: 34px; border-radius: 50%; background: linear-gradient(135deg, var(--brand), var(--brand-2)); color: #fff; font: 600 12px system-ui, sans-serif; display: grid; place-items: center; }
54
+ .section-label { display: flex; justify-content: space-between; align-items: baseline; padding: 4px 0 8px; }
55
+ .contacts { list-style: none; margin: 0; padding: 0; border-top: 1px solid var(--line); max-height: 300px; overflow: auto; }
56
+ .contact { display: flex; align-items: center; gap: 12px; width: 100%; padding: 9px 6px 9px 4px; border: 0; border-bottom: 1px solid var(--line); border-radius: 0; background: transparent; color: var(--ink); font: inherit; text-align: left; cursor: pointer; transition: background 0.1s ease, color 0.1s ease; }
57
+ .contact:hover:not(:disabled), .contact:focus-visible { background: var(--ink); color: var(--paper); outline: none; }
58
+ .contact:disabled { cursor: default; color: var(--muted); }
59
+ .contact .avatar { width: 28px; height: 28px; border: 1px solid currentColor; border-radius: 3px; display: grid; place-items: center; font: 10px var(--mono); letter-spacing: 0.06em; flex: none; }
67
60
  .contact .meta { flex: 1; min-width: 0; }
68
- .contact .name { display: block; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
69
- .contact .sub { display: block; color: var(--muted); font-size: 12px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
70
- .contact .arrow { flex: none; color: var(--muted); font-size: 16px; line-height: 1; }
61
+ .contact .name { display: block; font-weight: 500; letter-spacing: -0.01em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
62
+ .contact .sub { display: block; margin-top: 1px; font: 10px var(--mono); letter-spacing: 0.04em; opacity: 0.6; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
63
+ .contact .arrow { flex: none; font: 10px var(--mono); letter-spacing: 0.14em; text-transform: uppercase; opacity: 0.7; }
71
64
  .contact.busy .arrow { animation: pulse 0.9s ease-in-out infinite; }
72
- @keyframes pulse { 50% { opacity: 0.3; } }
73
- .footnote { padding: 12px 6px 4px; }
65
+ @keyframes pulse { 50% { opacity: 0.2; } }
66
+ .footnote { padding: 12px 0 4px; font-size: 11px; }
74
67
 
75
- footer { padding: 6px 16px 12px; min-height: 30px; }
76
- #status { font-size: 12px; color: var(--muted); overflow-wrap: anywhere; }
77
- #status[data-kind="error"] { color: var(--danger); }
78
- #status[data-kind="ok"] { color: #2f9e6a; }
68
+ footer { padding: 8px 14px 12px; min-height: 30px; border-top: 1px solid var(--line); }
69
+ #status { font: 10.5px var(--mono); letter-spacing: 0.03em; color: var(--muted); overflow-wrap: anywhere; }
70
+ #status[data-kind="error"] { color: var(--ink); text-decoration: underline; text-decoration-style: dotted; text-underline-offset: 3px; }
71
+ #status[data-kind="ok"] { color: var(--ink); }
@@ -8,7 +8,9 @@
8
8
  </head>
9
9
  <body>
10
10
  <header class="top">
11
- <img class="logo" src="icons/48.png" width="28" height="28" alt="">
11
+ <span class="mark" aria-hidden="true">
12
+ <svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.6"><circle cx="10" cy="7.5" r="3.2"/><path d="M4.5 16.5c.6-3.6 2.9-5.5 5.5-5.5s4.9 1.9 5.5 5.5"/></svg>
13
+ </span>
12
14
  <div class="brand">
13
15
  <h1>Contacts</h1>
14
16
  <p id="page" class="page">Checking this page…</p>
@@ -18,24 +20,25 @@
18
20
  <main>
19
21
  <section id="view-connecting" class="state">
20
22
  <span class="spinner" aria-hidden="true"></span>
21
- <p>Connecting to Contacts…</p>
23
+ <p class="label">Connecting</p>
22
24
  </section>
23
25
 
24
26
  <section id="view-setup" class="state" hidden>
25
27
  <h2>Not connected</h2>
26
- <p>Contacts uses the <code>contacts</code> CLI already on this station. Run <code>contacts browser install</code> in a terminal, then reopen Contacts.</p>
27
- <p id="setup-reason" class="muted"></p>
28
+ <p>Contacts uses the <code>contacts</code> CLI already on this station. Run <code>contacts browser install</code>, then reopen Contacts.</p>
29
+ <p id="setup-reason" class="muted small"></p>
28
30
  </section>
29
31
 
30
32
  <section id="view-contacts" hidden>
31
- <input id="query" class="search" type="search" placeholder="Search contacts…" autocomplete="off" aria-label="Search contacts">
33
+ <input id="query" class="search" type="search" placeholder="Search name, company, email" autocomplete="off" aria-label="Search contacts">
32
34
  <div id="page-actions" class="page-actions" hidden>
33
35
  <button id="detect-frames" class="chip-btn" type="button">Detect embedded fields</button>
34
36
  <span class="muted small">Chrome asks before Contacts looks inside embedded content.</span>
35
37
  </div>
38
+ <div class="section-label"><span class="label">Contacts</span><span class="label">Tap to fill</span></div>
36
39
  <ul id="contacts" class="contacts" aria-label="Contacts"></ul>
37
- <p id="empty" class="muted" hidden>No contacts match. Try another name, company or email.</p>
38
- <p class="muted small footnote">Tap a contact to fill name, email, phone, company and address fields. Nothing is submitted.</p>
40
+ <p id="empty" class="muted small" hidden>No contacts match. Try another name, company or email.</p>
41
+ <p class="muted footnote">Fills name, email, phone, company and address. Nothing is submitted.</p>
39
42
  </section>
40
43
  </main>
41
44
 
@@ -61,11 +61,11 @@ function initials(name) {
61
61
  }
62
62
  function pageLabel() {
63
63
  const page = state.page;
64
- if (!page) return { text: "Open a page with a form to fill", ready: false };
64
+ if (!page) return { text: "No form page", ready: false };
65
65
  const host = new URL(page.origin).host;
66
- if (page.fieldCount) return { text: `${host} · ${page.fieldCount} field${page.fieldCount === 1 ? "" : "s"} detected`, ready: true };
67
- if (page.blockedOrigins.includes(page.origin)) return { text: `${host} · allow access to detect fields`, ready: false };
68
- return { text: `${host} · no contact fields on this page`, ready: false };
66
+ if (page.fieldCount) return { text: `${host} · ${page.fieldCount} field${page.fieldCount === 1 ? "" : "s"}`, ready: true };
67
+ if (page.blockedOrigins.includes(page.origin)) return { text: `${host} · access needed`, ready: false };
68
+ return { text: `${host} · no contact fields`, ready: false };
69
69
  }
70
70
  function render() {
71
71
  const label = pageLabel();
@@ -100,7 +100,7 @@ function render() {
100
100
  meta.append(name, sub);
101
101
  const arrow = document.createElement("span");
102
102
  arrow.className = "arrow";
103
- arrow.textContent = "→";
103
+ arrow.textContent = "Fill →";
104
104
  btn.append(avatar, meta, arrow);
105
105
  btn.addEventListener("click", () => fill(contact, btn).catch(failure));
106
106
  li.appendChild(btn);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/contacts",
3
- "version": "0.9.0",
3
+ "version": "0.9.1",
4
4
  "description": "Contact management for AI coding agents — CLI + MCP + Web + Chrome autofill",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",