PageSourceSearch

https://app.replicants.ai/js/country-chip-picker.js?v=20260929m21

js replicants.ai collected 2026-10-05 12:30:06 UTC 23,329 bytes, 447 lines download raw bytes

1/**
2 * src/public/js/country-chip-picker.js
3 *
4 * Shared 10-chip country multi-select. Used by BOTH:
5 *   - setup.html step 4 (the SC1-deferred Targeting wizard step, landed SC5)
6 *   - discover.html (the legacy-customer country prompt at top of page)
7 *
8 * One source of truth so the chip UX doesn't drift between the two sites.
9 *
10 * Usage:
11 *   const picker = createCountryChipPicker({
12 *     mountEl: document.querySelector('#mountId'),
13 *     defaults: ['us','gb'],                 // pre-selected (optional)
14 *     onSubmit: async (countries) => { ... }, // called with array of ISO-2 codes
15 *     submitLabel: 'Continue',                // CTA button text
16 *   });
17 *   // picker exposes: getSelected(), submit() (programmatic), destroy()
18 *
19 * No framework — vanilla JS + inline styles. Loaded via plain <script src=...>.
20 * Per CODING-STANDARDS: no build step, no bundler.
21 *
22 * Spec: docs/migration/phase-DISCOVERY-onboarding.md §4 + §8.2.
23 */
24(function () {
25  // Top-10 default chips per spec §4.1. Order = popularity from the
26  // Replicants outreach data. ISO-3166-1 alpha-2 (lowercase by
27  // convention; the /set-countries endpoint lowercases anyway).
28  var DEFAULT_CHIPS = [
29    { code: 'us', label: 'United States' },
30    { code: 'gb', label: 'United Kingdom' },
31    { code: 'au', label: 'Australia' },
32    { code: 'ca', label: 'Canada' },
33    { code: 'ie', label: 'Ireland' },
34    { code: 'nz', label: 'New Zealand' },
35    { code: 'in', label: 'India' },
36    { code: 'de', label: 'Germany' },
37    { code: 'nl', label: 'Netherlands' },
38    { code: 'fr', label: 'France' },
39  ];
40
41  // SETUP-polish SC1 U2 — typeahead lookup. ISO-3166-1 alpha-2 (lowercase).
42  // ~250 entries inline; no external API. Used by the "Add another"
43  // typeahead to map typed country names → ISO codes.
44  var COUNTRY_LIST = [
45    { code: 'af', name: 'Afghanistan' }, { code: 'al', name: 'Albania' }, { code: 'dz', name: 'Algeria' },
46    { code: 'as', name: 'American Samoa' }, { code: 'ad', name: 'Andorra' }, { code: 'ao', name: 'Angola' },
47    { code: 'ai', name: 'Anguilla' }, { code: 'aq', name: 'Antarctica' }, { code: 'ag', name: 'Antigua and Barbuda' },
48    { code: 'ar', name: 'Argentina' }, { code: 'am', name: 'Armenia' }, { code: 'aw', name: 'Aruba' },
49    { code: 'au', name: 'Australia' }, { code: 'at', name: 'Austria' }, { code: 'az', name: 'Azerbaijan' },
50    { code: 'bs', name: 'Bahamas' }, { code: 'bh', name: 'Bahrain' }, { code: 'bd', name: 'Bangladesh' },
51    { code: 'bb', name: 'Barbados' }, { code: 'by', name: 'Belarus' }, { code: 'be', name: 'Belgium' },
52    { code: 'bz', name: 'Belize' }, { code: 'bj', name: 'Benin' }, { code: 'bm', name: 'Bermuda' },
53    { code: 'bt', name: 'Bhutan' }, { code: 'bo', name: 'Bolivia' }, { code: 'ba', name: 'Bosnia and Herzegovina' },
54    { code: 'bw', name: 'Botswana' }, { code: 'br', name: 'Brazil' }, { code: 'io', name: 'British Indian Ocean Territory' },
55    { code: 'vg', name: 'British Virgin Islands' }, { code: 'bn', name: 'Brunei' }, { code: 'bg', name: 'Bulgaria' },
56    { code: 'bf', name: 'Burkina Faso' }, { code: 'bi', name: 'Burundi' }, { code: 'kh', name: 'Cambodia' },
57    { code: 'cm', name: 'Cameroon' }, { code: 'ca', name: 'Canada' }, { code: 'cv', name: 'Cape Verde' },
58    { code: 'ky', name: 'Cayman Islands' }, { code: 'cf', name: 'Central African Republic' }, { code: 'td', name: 'Chad' },
59    { code: 'cl', name: 'Chile' }, { code: 'cn', name: 'China' }, { code: 'cx', name: 'Christmas Island' },
60    { code: 'cc', name: 'Cocos Islands' }, { code: 'co', name: 'Colombia' }, { code: 'km', name: 'Comoros' },
61    { code: 'ck', name: 'Cook Islands' }, { code: 'cr', name: 'Costa Rica' }, { code: 'hr', name: 'Croatia' },
62    { code: 'cu', name: 'Cuba' }, { code: 'cw', name: 'Curaçao' }, { code: 'cy', name: 'Cyprus' },
63    { code: 'cz', name: 'Czech Republic' }, { code: 'cd', name: 'Democratic Republic of the Congo' },
64    { code: 'dk', name: 'Denmark' }, { code: 'dj', name: 'Djibouti' }, { code: 'dm', name: 'Dominica' },
65    { code: 'do', name: 'Dominican Republic' }, { code: 'tl', name: 'East Timor' }, { code: 'ec', name: 'Ecuador' },
66    { code: 'eg', name: 'Egypt' }, { code: 'sv', name: 'El Salvador' }, { code: 'gq', name: 'Equatorial Guinea' },
67    { code: 'er', name: 'Eritrea' }, { code: 'ee', name: 'Estonia' }, { code: 'et', name: 'Ethiopia' },
68    { code: 'fk', name: 'Falkland Islands' }, { code: 'fo', name: 'Faroe Islands' }, { code: 'fj', name: 'Fiji' },
69    { code: 'fi', name: 'Finland' }, { code: 'fr', name: 'France' }, { code: 'pf', name: 'French Polynesia' },
70    { code: 'ga', name: 'Gabon' }, { code: 'gm', name: 'Gambia' }, { code: 'ge', name: 'Georgia' },
71    { code: 'de', name: 'Germany' }, { code: 'gh', name: 'Ghana' }, { code: 'gi', name: 'Gibraltar' },
72    { code: 'gr', name: 'Greece' }, { code: 'gl', name: 'Greenland' }, { code: 'gd', name: 'Grenada' },
73    { code: 'gu', name: 'Guam' }, { code: 'gt', name: 'Guatemala' }, { code: 'gg', name: 'Guernsey' },
74    { code: 'gn', name: 'Guinea' }, { code: 'gw', name: 'Guinea-Bissau' }, { code: 'gy', name: 'Guyana' },
75    { code: 'ht', name: 'Haiti' }, { code: 'hn', name: 'Honduras' }, { code: 'hk', name: 'Hong Kong' },
76    { code: 'hu', name: 'Hungary' }, { code: 'is', name: 'Iceland' }, { code: 'in', name: 'India' },
77    { code: 'id', name: 'Indonesia' }, { code: 'ir', name: 'Iran' }, { code: 'iq', name: 'Iraq' },
78    { code: 'ie', name: 'Ireland' }, { code: 'im', name: 'Isle of Man' }, { code: 'il', name: 'Israel' },
79    { code: 'it', name: 'Italy' }, { code: 'ci', name: 'Ivory Coast' }, { code: 'jm', name: 'Jamaica' },
80    { code: 'jp', name: 'Japan' }, { code: 'je', name: 'Jersey' }, { code: 'jo', name: 'Jordan' },
81    { code: 'kz', name: 'Kazakhstan' }, { code: 'ke', name: 'Kenya' }, { code: 'ki', name: 'Kiribati' },
82    { code: 'xk', name: 'Kosovo' }, { code: 'kw', name: 'Kuwait' }, { code: 'kg', name: 'Kyrgyzstan' },
83    { code: 'la', name: 'Laos' }, { code: 'lv', name: 'Latvia' }, { code: 'lb', name: 'Lebanon' },
84    { code: 'ls', name: 'Lesotho' }, { code: 'lr', name: 'Liberia' }, { code: 'ly', name: 'Libya' },
85    { code: 'li', name: 'Liechtenstein' }, { code: 'lt', name: 'Lithuania' }, { code: 'lu', name: 'Luxembourg' },
86    { code: 'mo', name: 'Macao' }, { code: 'mk', name: 'North Macedonia' }, { code: 'mg', name: 'Madagascar' },
87    { code: 'mw', name: 'Malawi' }, { code: 'my', name: 'Malaysia' }, { code: 'mv', name: 'Maldives' },
88    { code: 'ml', name: 'Mali' }, { code: 'mt', name: 'Malta' }, { code: 'mh', name: 'Marshall Islands' },
89    { code: 'mr', name: 'Mauritania' }, { code: 'mu', name: 'Mauritius' }, { code: 'yt', name: 'Mayotte' },
90    { code: 'mx', name: 'Mexico' }, { code: 'fm', name: 'Micronesia' }, { code: 'md', name: 'Moldova' },
91    { code: 'mc', name: 'Monaco' }, { code: 'mn', name: 'Mongolia' }, { code: 'me', name: 'Montenegro' },
92    { code: 'ms', name: 'Montserrat' }, { code: 'ma', name: 'Morocco' }, { code: 'mz', name: 'Mozambique' },
93    { code: 'mm', name: 'Myanmar' }, { code: 'na', name: 'Namibia' }, { code: 'nr', name: 'Nauru' },
94    { code: 'np', name: 'Nepal' }, { code: 'nl', name: 'Netherlands' }, { code: 'nc', name: 'New Caledonia' },
95    { code: 'nz', name: 'New Zealand' }, { code: 'ni', name: 'Nicaragua' }, { code: 'ne', name: 'Niger' },
96    { code: 'ng', name: 'Nigeria' }, { code: 'nu', name: 'Niue' }, { code: 'kp', name: 'North Korea' },
97    { code: 'no', name: 'Norway' }, { code: 'om', name: 'Oman' }, { code: 'pk', name: 'Pakistan' },
98    { code: 'pw', name: 'Palau' }, { code: 'ps', name: 'Palestine' }, { code: 'pa', name: 'Panama' },
99    { code: 'pg', name: 'Papua New Guinea' }, { code: 'py', name: 'Paraguay' }, { code: 'pe', name: 'Peru' },
100    { code: 'ph', name: 'Philippines' }, { code: 'pl', name: 'Poland' }, { code: 'pt', name: 'Portugal' },
101    { code: 'pr', name: 'Puerto Rico' }, { code: 'qa', name: 'Qatar' }, { code: 'cg', name: 'Republic of the Congo' },
102    { code: 're', name: 'Réunion' }, { code: 'ro', name: 'Romania' }, { code: 'ru', name: 'Russia' },
103    { code: 'rw', name: 'Rwanda' }, { code: 'bl', name: 'Saint Barthélemy' }, { code: 'sh', name: 'Saint Helena' },
104    { code: 'kn', name: 'Saint Kitts and Nevis' }, { code: 'lc', name: 'Saint Lucia' }, { code: 'mf', name: 'Saint Martin' },
105    { code: 'pm', name: 'Saint Pierre and Miquelon' }, { code: 'vc', name: 'Saint Vincent and the Grenadines' },
106    { code: 'ws', name: 'Samoa' }, { code: 'sm', name: 'San Marino' }, { code: 'st', name: 'São Tomé and Príncipe' },
107    { code: 'sa', name: 'Saudi Arabia' }, { code: 'sn', name: 'Senegal' }, { code: 'rs', name: 'Serbia' },
108    { code: 'sc', name: 'Seychelles' }, { code: 'sl', name: 'Sierra Leone' }, { code: 'sg', name: 'Singapore' },
109    { code: 'sx', name: 'Sint Maarten' }, { code: 'sk', name: 'Slovakia' }, { code: 'si', name: 'Slovenia' },
110    { code: 'sb', name: 'Solomon Islands' }, { code: 'so', name: 'Somalia' }, { code: 'za', name: 'South Africa' },
111    { code: 'kr', name: 'South Korea' }, { code: 'ss', name: 'South Sudan' }, { code: 'es', name: 'Spain' },
112    { code: 'lk', name: 'Sri Lanka' }, { code: 'sd', name: 'Sudan' }, { code: 'sr', name: 'Suriname' },
113    { code: 'sz', name: 'Eswatini' }, { code: 'se', name: 'Sweden' }, { code: 'ch', name: 'Switzerland' },
114    { code: 'sy', name: 'Syria' }, { code: 'tw', name: 'Taiwan' }, { code: 'tj', name: 'Tajikistan' },
115    { code: 'tz', name: 'Tanzania' }, { code: 'th', name: 'Thailand' }, { code: 'tg', name: 'Togo' },
116    { code: 'tk', name: 'Tokelau' }, { code: 'to', name: 'Tonga' }, { code: 'tt', name: 'Trinidad and Tobago' },
117    { code: 'tn', name: 'Tunisia' }, { code: 'tr', name: 'Turkey' }, { code: 'tm', name: 'Turkmenistan' },
118    { code: 'tc', name: 'Turks and Caicos Islands' }, { code: 'tv', name: 'Tuvalu' }, { code: 'ug', name: 'Uganda' },
119    { code: 'ua', name: 'Ukraine' }, { code: 'ae', name: 'United Arab Emirates' }, { code: 'gb', name: 'United Kingdom' },
120    { code: 'us', name: 'United States' }, { code: 'uy', name: 'Uruguay' }, { code: 'uz', name: 'Uzbekistan' },
121    { code: 'vu', name: 'Vanuatu' }, { code: 'va', name: 'Vatican City' }, { code: 've', name: 'Venezuela' },
122    { code: 'vn', name: 'Vietnam' }, { code: 'wf', name: 'Wallis and Futuna' }, { code: 'eh', name: 'Western Sahara' },
123    { code: 'ye', name: 'Yemen' }, { code: 'zm', name: 'Zambia' }, { code: 'zw', name: 'Zimbabwe' },
124  ];
125
126  function createCountryChipPicker(opts) {
127    opts = opts || {};
128    if (!opts.mountEl) throw new Error('country-chip-picker: mountEl required');
129    // hideSubmit: render chips only, no submit button + no onSubmit needed.
130    // Caller reads selection via getSelected() and submits on its own
131    // schedule (used by community-criteria-form.js embedded mode).
132    if (!opts.hideSubmit && typeof opts.onSubmit !== 'function') {
133      throw new Error('country-chip-picker: onSubmit required');
134    }
135
136    var defaults = Array.isArray(opts.defaults) ? opts.defaults.map(function (s) { return String(s).toLowerCase(); }) : [];
137    var submitLabel = opts.submitLabel || 'Continue';
138    var selected = new Set(defaults);
139    var customCodes = [];   // user-added codes outside the default 10
140
141    // Pre-populate any default codes that aren't in DEFAULT_CHIPS — they
142    // become custom chips so the widget shows them as selected.
143    var defaultCodes = DEFAULT_CHIPS.map(function (c) { return c.code; });
144    defaults.forEach(function (c) {
145      if (defaultCodes.indexOf(c) === -1 && customCodes.indexOf(c) === -1) {
146        customCodes.push(c);
147      }
148    });
149
150    // Root container with scoped class for styling. Caller's mountEl
151    // typically has its own outer styling; the widget injects this
152    // inside.
153    var root = document.createElement('div');
154    root.className = 'ccp-root';
155    root.style.cssText = 'display:flex;flex-direction:column;gap:16px;';
156
157    // Chips
158    var chipsRow = document.createElement('div');
159    chipsRow.className = 'ccp-chips';
160    chipsRow.style.cssText = 'display:flex;flex-wrap:wrap;gap:8px;';
161
162    function makeChip(code, label) {
163      var btn = document.createElement('button');
164      btn.type = 'button';
165      btn.className = 'ccp-chip';
166      btn.dataset.code = code;
167      btn.textContent = label;
168      btn.style.cssText = [
169        'padding:8px 14px',
170        'border-radius:999px',
171        'border:1.5px solid #d4d4d8',
172        'background:#fff',
173        'color:#27272a',
174        'font-size:14px',
175        'font-weight:500',
176        'cursor:pointer',
177        'transition:all 0.15s ease',
178      ].join(';');
179      function updateVisual() {
180        var on = selected.has(code);
181        btn.style.background = on ? '#6a4cf1' : '#fff';
182        btn.style.borderColor = on ? '#6a4cf1' : '#d4d4d8';
183        btn.style.color = on ? '#fff' : '#27272a';
184        btn.setAttribute('aria-pressed', String(on));
185      }
186      btn.addEventListener('click', function () {
187        if (selected.has(code)) selected.delete(code);
188        else selected.add(code);
189        updateVisual();
190        _refreshSubmitButton();
191      });
192      updateVisual();
193      return btn;
194    }
195
196    DEFAULT_CHIPS.forEach(function (c) {
197      chipsRow.appendChild(makeChip(c.code, c.label));
198    });
199    customCodes.forEach(function (c) {
200      chipsRow.appendChild(makeChip(c, c.toUpperCase()));
201    });
202
203    // "+ Add another" row — SETUP-polish SC1 U2: typeahead. User types a
204    // country name (or partial); a dropdown shows matches from COUNTRY_LIST;
205    // clicking a suggestion adds the chip selected. Direct ISO-2 paste also
206    // still works (backward compat).
207    var addRow = document.createElement('div');
208    addRow.style.cssText = 'display:flex;gap:8px;align-items:center;position:relative;';
209    var addInput = document.createElement('input');
210    addInput.type = 'text';
211    addInput.placeholder = 'Add another country: type a name…';
212    addInput.style.cssText = [
213      'flex:1',
214      'padding:8px 12px',
215      'border:1.5px solid #d4d4d8',
216      'border-radius:8px',
217      'font-size:14px',
218    ].join(';');
219    var addBtn = document.createElement('button');
220    addBtn.type = 'button';
221    addBtn.textContent = '+ Add';
222    addBtn.style.cssText = [
223      'padding:8px 14px',
224      'border-radius:8px',
225      'border:1.5px solid #6a4cf1',
226      'background:#fff',
227      'color:#6a4cf1',
228      'font-size:14px',
229      'font-weight:600',
230      'cursor:pointer',
231    ].join(';');
232
233    // Typeahead dropdown (positioned below the input).
234    var dropdown = document.createElement('div');
235    dropdown.style.cssText = [
236      'position:absolute',
237      'top:100%',
238      'left:0',
239      'right:60px',          // leave room for the + Add button
240      'margin-top:4px',
241      'max-height:220px',
242      'overflow-y:auto',
243      'background:#fff',
244      'border:1.5px solid #d4d4d8',
245      'border-radius:8px',
246      'box-shadow:0 6px 20px -8px rgba(0,0,0,0.15)',
247      'z-index:50',
248      'display:none',
249    ].join(';');
250
251    function _addCode(v) {
252      // Add the lowercased ISO code v as a selected chip. Idempotent.
253      if (defaultCodes.indexOf(v) !== -1 || customCodes.indexOf(v) !== -1) {
254        selected.add(v);
255        chipsRow.querySelectorAll('.ccp-chip').forEach(function (c) {
256          if (c.dataset.code === v) {
257            c.style.background = '#6a4cf1';
258            c.style.borderColor = '#6a4cf1';
259            c.style.color = '#fff';
260            c.setAttribute('aria-pressed', 'true');
261          }
262        });
263      } else {
264        customCodes.push(v);
265        selected.add(v);
266        var chip = makeChip(v, v.toUpperCase());
267        chipsRow.appendChild(chip);
268      }
269      addInput.value = '';
270      dropdown.style.display = 'none';
271      _refreshSubmitButton();
272    }
273
274    function _renderDropdown(query) {
275      var q = (query || '').trim().toLowerCase();
276      dropdown.innerHTML = '';
277      if (q.length < 1) { dropdown.style.display = 'none'; return; }
278      // Direct ISO-2 shortcut: typed exactly 2 letters that match a country code.
279      if (/^[a-z]{2}$/.test(q)) {
280        var direct = COUNTRY_LIST.filter(function (c) { return c.code === q; });
281        if (direct.length === 1) {
282          _renderRow(dropdown, direct[0]);
283          dropdown.style.display = 'block';
284          return;
285        }
286      }
287      var matches = COUNTRY_LIST.filter(function (c) {
288        return c.name.toLowerCase().indexOf(q) !== -1 || c.code.indexOf(q) === 0;
289      }).slice(0, 8);
290      if (matches.length === 0) { dropdown.style.display = 'none'; return; }
291      matches.forEach(function (m) { _renderRow(dropdown, m); });
292      dropdown.style.display = 'block';
293    }
294
295    function _renderRow(parent, m) {
296      var row = document.createElement('div');
297      row.style.cssText = 'padding:8px 12px;cursor:pointer;font-size:14px;color:#27272a;display:flex;justify-content:space-between;align-items:center;';
298      row.addEventListener('mouseenter', function () { row.style.background = '#f4f4f5'; });
299      row.addEventListener('mouseleave', function () { row.style.background = '#fff'; });
300      var name = document.createElement('span');
301      name.textContent = m.name;
302      var code = document.createElement('span');
303      code.textContent = m.code.toUpperCase();
304      code.style.cssText = 'font-family:"JetBrains Mono",monospace;font-size:12px;color:#a1a1aa;';
305      row.appendChild(name);
306      row.appendChild(code);
307      row.addEventListener('click', function () { _addCode(m.code); });
308      parent.appendChild(row);
309    }
310
311    function tryAdd() {
312      var v = (addInput.value || '').trim().toLowerCase();
313      // Try direct ISO-2 code first (backward compat).
314      if (/^[a-z]{2}$/.test(v) && COUNTRY_LIST.some(function (c) { return c.code === v; })) {
315        _addCode(v);
316        return;
317      }
318      // Otherwise pick the first dropdown match.
319      var firstMatch = COUNTRY_LIST.filter(function (c) {
320        return c.name.toLowerCase().indexOf(v) !== -1;
321      })[0];
322      if (firstMatch) { _addCode(firstMatch.code); return; }
323      addInput.style.borderColor = '#dc2626';
324      setTimeout(function () { addInput.style.borderColor = '#d4d4d8'; }, 1500);
325    }
326    addInput.addEventListener('input', function () { _renderDropdown(addInput.value); });
327    addInput.addEventListener('focus', function () { _renderDropdown(addInput.value); });
328    addInput.addEventListener('blur', function () {
329      // Delay so a click on a dropdown row registers before hide.
330      setTimeout(function () { dropdown.style.display = 'none'; }, 200);
331    });
332    addBtn.addEventListener('click', tryAdd);
333    addInput.addEventListener('keydown', function (e) {
334      if (e.key === 'Enter') { e.preventDefault(); tryAdd(); }
335    });
336    addRow.appendChild(addInput);
337    addRow.appendChild(addBtn);
338    addRow.appendChild(dropdown);
339
340    // Submit button (omitted in hideSubmit mode)
341    var submitBtn = null;
342    function _refreshSubmitButton() {
343      if (!submitBtn) return;
344      var n = selected.size;
345      submitBtn.disabled = n < 1;
346      submitBtn.style.opacity = n < 1 ? '0.5' : '1';
347      submitBtn.style.cursor = n < 1 ? 'not-allowed' : 'pointer';
348    }
349    if (!opts.hideSubmit) {
350      submitBtn = document.createElement('button');
351      submitBtn.type = 'button';
352      submitBtn.textContent = submitLabel;
353      submitBtn.style.cssText = [
354        'align-self:flex-start',
355        'padding:10px 20px',
356        'border-radius:10px',
357        'border:none',
358        'background:#6a4cf1',
359        'color:#fff',
360        'font-size:15px',
361        'font-weight:600',
362        'cursor:pointer',
363        'transition:opacity 0.15s ease',
364      ].join(';');
365      _refreshSubmitButton();
366      var submitting = false;
367      submitBtn.addEventListener('click', function () {
368        if (submitting || selected.size < 1) return;
369        submitting = true;
370        submitBtn.disabled = true;
371        submitBtn.style.opacity = '0.6';
372        var prevLabel = submitBtn.textContent;
373        submitBtn.textContent = 'Saving…';
374        Promise.resolve(opts.onSubmit(Array.from(selected)))
375          .catch(function (err) {
376            console.warn('[country-chip-picker] onSubmit threw —', err && err.message);
377            submitting = false;
378            submitBtn.disabled = false;
379            submitBtn.style.opacity = '1';
380            submitBtn.textContent = prevLabel;
381          });
382        // On success the caller is responsible for any post-submit UX (e.g.
383        // advancing the wizard or replacing the prompt with a banner).
384      });
385    }
386
387    root.appendChild(chipsRow);
388    root.appendChild(addRow);
389    if (submitBtn) root.appendChild(submitBtn);
390    opts.mountEl.appendChild(root);
391
392    return {
393      getSelected: function () { return Array.from(selected); },
394      submit: function () { if (submitBtn) submitBtn.click(); },
395      destroy: function () { if (root.parentNode) root.parentNode.removeChild(root); },
396    };
397  }
398
399  // Expose on window for both setup.html and discover.html to use.
400  window.createCountryChipPicker = createCountryChipPicker;
401
402  // ── Name → ISO-2 helpers (CI onboarding prefill) ──────────────────────
403  // CI personas store their target countries as free text in a single
404  // field (migrated from Bubble). To pre-select the chip picker we need to
405  // map those names to ISO-2 codes. COUNTRY_LIST covers canonical names;
406  // this alias map covers the common shorthands / spellings seen in the
407  // live CI data (UAE, KSA, Bahrein, Türkiye, …). Region words and city
408  // names that don't resolve to a single country are intentionally skipped.
409  var COUNTRY_ALIASES = {
410    'uae': 'ae', 'u.a.e': 'ae', 'u.a.e.': 'ae', 'emirates': 'ae',
411    'ksa': 'sa', 'kingdom of saudi arabia': 'sa', 'saudi': 'sa', 'saudi arabia ': 'sa',
412    'bahrein': 'bh',
413    'türkiye': 'tr', 'turkiye': 'tr',
414    'uk': 'gb', 'u.k': 'gb', 'u.k.': 'gb', 'great britain': 'gb', 'england': 'gb',
415    'usa': 'us', 'u.s.a': 'us', 'u.s.a.': 'us', 'u.s': 'us', 'us': 'us', 'america': 'us',
416    'south korea ': 'kr', 'korea': 'kr',
417    'czechia': 'cz',
418  };
419
420  function countryNameToCode(raw) {
421    var n = String(raw || '').trim().toLowerCase().replace(/\.$/, '');
422    if (!n) return null;
423    if (/^[a-z]{2}$/.test(n) && COUNTRY_LIST.some(function (c) { return c.code === n; })) return n;
424    if (COUNTRY_ALIASES[n]) return COUNTRY_ALIASES[n];
425    var exact = COUNTRY_LIST.filter(function (c) { return c.name.toLowerCase() === n; })[0];
426    if (exact) return exact.code;
427    var starts = COUNTRY_LIST.filter(function (c) { return c.name.toLowerCase().indexOf(n) === 0 && n.length >= 4; })[0];
428    if (starts) return starts.code;
429    return null;
430  }
431
432  // Parse a free-text list of countries ("Saudi Arabia, Australia, UAE,
433  // Bahrein,") into a de-duped array of ISO-2 codes. Splits on commas,
434  // slashes and semicolons. Unresolvable tokens (regions like "Middle
435  // East", cities like "Copenhagen") are dropped silently.
436  function parseCountriesToCodes(text) {
437    var out = [];
438    String(text || '').split(/[,/;]+/).forEach(function (tok) {
439      var code = countryNameToCode(tok);
440      if (code && out.indexOf(code) === -1) out.push(code);
441    });
442    return out;
443  }
444
445  window.countryNameToCode = countryNameToCode;
446  window.parseCountriesToCodes = parseCountriesToCodes;
447})();

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.