Native Shell
Native Shell은 모든 플랫폼의 앱 안에서 만들어지는 AI UI입니다
이미 가진 명단에 사람을 추가하면 Goldpan이 중복을 제거하고 오타를 바로잡아 남은 위치에 저장합니다
Add people to a list you already have: Goldpan spots who is already in, drops duplicates, fixes typos in one click and keeps to the places left.
개발 도구TypeScript · JavaScriptGitHub @molinariCode온라인운영자 미확인
써 보셨어요? 한 번만 눌러 주세요. 로그인은 필요 없어요.
개인 계정의 공개 저장소 소유자는 GitHub로 로그인해 운영자를 확인하고 소개를 직접 관리할 수 있습니다. 관리자 승인은 필요하지 않습니다.
로그인한 뒤 확인 파일을 만들어 이 저장소의 기본 브랜치에 30분 안에 커밋해 주세요. 다른 브랜치나 포크의 파일은 인정하지 않습니다. 파일을 올린 뒤 아래에서 확인하면 관리자 승인 없이 관리할 수 있습니다. 자격을 유지하려면 파일을 남겨 두세요.
로그인이 필요하면 로그인 화면으로 이동합니다. 로그인 후 이곳으로 돌아와 원하는 확인 버튼을 다시 눌러 주세요.
프로젝트 폴더에서 /nomorevibe verify를 실행하면 소유권을 확인하고 소개를 직접 관리할 수 있습니다.
1분 만에 확인하기Add new email addresses to a list you already have and keep only the gold: people already in and duplicates spotted, typos fixed in one click, places left respected. Tiny, dependency-free, accessible multi-email input.
저장소 README에서 가져옴
README 전체를 불러오는 중입니다.
README 전문 보기 ↗New addresses for your list. Only the gold gets in.
A tiny, dependency-free multiple email input for adding people to a list you already have: team members,
subscribers, guests. Paste the new addresses from anywhere and it spots who is already in, drops duplicates, offers a
one-click fix for misspelled domains (gmial.com), stops at the places left and keeps your form in sync.
Most "email tag inputs" only say whether an address is well formed. Adding people to a list you already have is messier: a column copied from Excel, a line from Outlook with names in it, the same person twice, people who are already members, a provider spelled wrong, more people than places left.
Gold panning is the same job: you pour in gravel and keep the nuggets. Goldpan gives every address exactly one status:
| Status | Shown as | In getEmails() | Typical reasons |
|---|---|---|---|
valid | green, Valid | ✅ | everything checks out |
warning | amber, To check | ✅ (takes a place under maxEmails) | probable typo, already registered, your own rule's warning |
invalid | red, Rejected | ❌ | malformed, excluded or certainly misspelled domain, no place left |
Only red is left out. Amber addresses are accepted and flagged so a person can take a second look.
existingEmails)Name <address>
(Outlook, Gmail, Apple Mail), mailto: links, full-width separators (, ; 、)gmial.com, hotmial.com, gmail.con…) with a one-click fix,
tuned to leave real domains alone: regional ones like hotmail.be or yahoo.in, and companies such as aon.commaxEmails: when a place frees up, the first address waiting moves in*.domain wildcards, ready for large disposable-domain listsvalidate callbackrejected option)<input> always holds the accepted addresses, and a submit with rejected addresses
works the same way; honours required, disabled and form resetaria-pressed filters, polite live region, aria-invalid, no keyboard
traps, input methods (Chinese, Japanese, Android keyboards) handled, Windows high-contrast modenpm install goldpan
import Goldpan from 'goldpan';
import 'goldpan/style.css';
Or with <script> tags, no build step:
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/goldpan@1/dist/goldpan.min.css">
<script src="https://cdn.jsdelivr.net/npm/goldpan@1/dist/goldpan.global.min.js"></script>
<!-- optional: languages -->
<script src="https://cdn.jsdelivr.net/npm/goldpan@1/dist/locales/it.global.min.js"></script>
<label for="recipients">Recipients</label>
<input id="recipients" name="recipients" required>
const pan = new Goldpan('#recipients', {
maxEmails: 50,
existingEmails: ['[email protected]'],
});
pan.on('change', ({ emails, counts, hasErrors }) => {
console.log(emails); // accepted addresses (valid and to check), normalized
});
The original input is hidden and kept in sync ("[email protected], [email protected]"), so a plain form submit just works.
| Option | Type | Default | Description |
|---|---|---|---|
maxEmails | number | null | null | Most accepted addresses (valid and to check). null = no limit. |
onLimit | 'reject' | 'drop' | 'reject' | Beyond the limit: show in red, or do not add. |
duplicates | 'drop' | 'warn' | 'reject' | 'drop' | The same address twice. drop keeps the first one and highlights it; warn keeps both, flagged for a check, each taking a place; reject shows the copy in red. |
existingEmails | Iterable<string> | [] | Addresses already registered on your side. |
existing | 'warn' | 'reject' | 'warn' | What to do with existingEmails matches. |
blockedDomains | Iterable<string> | Goldpan.blockedDomains | Exclusion list, always red. Passing a list replaces the default (see below). |
typoCheck | 'warn' | 'reject' | false | 'warn' | Probable misspellings of popular providers. Both policies offer the fix button. |
knownDomains | Iterable<string> | [] | More correct domains for the typo check (also stops look-alikes from being reported). |
validate | (email) => null | { status, message } | – | Your own rule; return 'warning' or 'invalid' to flag an address. |
locale | string | object | 'en' | Registered language code or a messages object. |
Every option can be changed later with setOptions(); statuses are recomputed at once.
const pan = new Goldpan(target: string | HTMLElement, options?: GoldpanOptions);
pan.getEmails(): string[] // accepted (valid and to check)
pan.getEntries(status?): GoldpanEntry[] // { id, email, raw, status, reason, message, suggestion }
pan.getCounts(): { valid, warning, invalid, admitted, remaining }
pan.hasErrors(): boolean
pan.getState(): { emails, entries, counts, hasErrors }
pan.add(text | string[]): GoldpanEntry[] // parsed like a paste
pan.setValue(text | string[]): GoldpanEntry[] // replaces the whole list
pan.update(idOrEmail, text): GoldpanEntry | null
pan.remove(idOrEmail): boolean
pan.clear(): void
pan.confirm(): boolean // call it from your send button: true = go on
pan.setOptions(partial): void
pan.setFilter('all' | 'valid' | 'warning' | 'invalid'): void
pan.focus() / pan.enable() / pan.disable()
pan.destroy(): void // gives the original input back
pan.on(event, listener): () => void // returns an unsubscribe function
pan.off(event, listener): void
Goldpan.init(selector, options): Goldpan[]
Goldpan.getInstance(element | selector): Goldpan | undefined
Goldpan.registerLocale(code, messages): void
Goldpan.parse(text): string[]
Goldpan.isValidFormat(email): boolean
target can be an <input> or <textarea> (recommended, kept in sync) or any container element.
| Event | Payload |
|---|---|
change | { emails, entries, counts, hasErrors } |
add | { entries } |
remove | { entry } |
update | { before, after } |
duplicate | { dropped: string[] } |
limit | { rejected: string[] } |
filter | { filter } |
confirm | { state, message, label }: a confirmation stopped on rejected addresses, changed or ended |
The same events are dispatched as DOM CustomEvents on the original element, prefixed with goldpan:
(goldpan:change, goldpan:add…), so you can listen from anywhere:
document.addEventListener('goldpan:change', (e) => console.log(e.detail.emails));
Call confirm() from the button that uses the list: true means the accepted addresses can go. While rejected
addresses are present, the rejected option decides:
rejected | First press | Next press |
|---|---|---|
'ask' (default) | shows the rejected addresses and how many they are: false | goes on without them: true |
'block' | shows them, asking to fix or remove them: false | the same, until they are fixed |
'skip' | goes on without them: true |
The widget writes the message above the list. To show it next to your button instead, as the demo does, turn
inlineMessage off and follow the confirm event: it carries the message and, while the next press goes on, a label
for your button ('Ignore and send', in the language of the widget).
const pan = new Goldpan('#to', { inlineMessage: false });
pan.on('confirm', ({ state, message, label }) => {
note.textContent = message; // '' when nothing is pending
sendButton.textContent = state === 'ask' ? label : 'Send';
});
sendButton.onclick = () => {
if (pan.confirm()) send(pan.getEmails()); // accepted addresses only
};
A form submit counts as a confirmation too: with 'ask' the first submit stops and the next one goes through, with
'block' native validation stops it. The message follows the list (it counts what is left and goes away when nothing
is rejected any more), and a second press right after the first one, a double click, does not count.
const pan = new Goldpan('#invite', {
maxEmails: plan.seats - members.length, // places left
existingEmails: members, // shown as "Already registered"
});
inviteButton.onclick = async () => {
if (!pan.confirm()) return; // rejected addresses: shown first, the next press goes on without them
members.push(...(await sendInvites(pan.getEmails())));
pan.clear();
pan.setOptions({ maxEmails: plan.seats - members.length, existingEmails: members });
};
new Goldpan('#invite', {
existing: 'reject',
existingEmails: currentMembers,
validate: (email) =>
email.endsWith('@acme.com') ? null : { status: 'invalid', message: 'Only @acme.com addresses' },
});
Addresses on these domains are always red. The built-in list (DEFAULT_BLOCKED_DOMAINS in
src/domains.ts) holds misspellings so common they are certainly wrong (gmal.com,
hotmial.com…, still shown with the fix button) and well-known throw-away services (mailinator.com, yopmail.com…).
// Extend it for every instance (before creating them)
Goldpan.blockedDomains.push('competitor.com', '*.tempmail.dev');
// Extend it for one instance
new Goldpan('#to', { blockedDomains: [...Goldpan.blockedDomains, 'competitor.com'] });
// Replace it, or turn it off
new Goldpan('#to', { blockedDomains: ['competitor.com'] });
new Goldpan('#to', { blockedDomains: [] });
// Change it at runtime
pan.setOptions({ blockedDomains: [...Goldpan.blockedDomains, 'new-spam.com'] });
// A large external list (e.g. the 4,000+ domains of disposable-email-domains)
new Goldpan('#to', { blockedDomains: new Set([...Goldpan.blockedDomains, ...bigList]) });
*.domain.com covers the domain and all its sub-domains; *.xyz a whole top-level domain. For rules a list cannot
express, use validate. A maintained list of throw-away domains:
disposable-email-domains.
import Goldpan from 'goldpan';
import it from 'goldpan/locales/it';
Goldpan.registerLocale('it', it);
const pan = new Goldpan('#to', { locale: 'it' });
// or replace a few texts
new Goldpan('#to', { messages: { duplicate: 'Already invited' } });
Bundled: en (default), it, es, fr, de, pt, zh, ja. goldpan/locales/all exports every language.
With <script> tags, load dist/locales/<code>.global.min.js after the core.
The counter uses the plural rules of the language (1 address, 2 addresses): a message can be a string or an
object with one text per plural category ({ one: '…', other: '…' }).
New languages are very welcome: copy src/locales/en.ts, translate, add it to src/locales/all.ts.
Goldpan works on any input element. Create it after mount, destroy it on unmount:
function Recipients({ onChange }) {
const ref = useRef(null);
useEffect(() => {
const pan = new Goldpan(ref.current, { onChange: (s) => onChange(s.emails) });
return () => pan.destroy();
}, []);
return <input ref={ref} name="recipients" />;
}
To set the list from outside (a saved draft, a "reset" button), call pan.setValue(addresses).
All classes start with goldpan. Customise with CSS variables:
.goldpan {
--goldpan-font: 'Inter', sans-serif;
--goldpan-font-size: 1rem; /* every size of the widget follows it */
--goldpan-radius: 6px; /* corners of the address cards */
--goldpan-list-max-height: 240px;
--goldpan-focus: #7c3aed; /* focus ring, highlighted card */
--goldpan-fg: #1b1f24; --goldpan-muted: #5b6470;
--goldpan-bg: #ffffff; --goldpan-row: #f5f6f8; /* text box, address cards */
--goldpan-line: #dfe3e8; --goldpan-edge: #838d99; /* card border, text box border */
--goldpan-valid: #0f7a5a; --goldpan-valid-soft: #e6f4ee;
--goldpan-warning: #8a5a00; --goldpan-warning-soft: #fff4d6; --goldpan-warning-line: #e8c36b;
--goldpan-invalid: #b42318; --goldpan-invalid-soft: #fef0ee; --goldpan-invalid-line: #f1a69d;
}
theme: 'dark' switches every colour to a dark set, theme: 'auto' follows the operating system. The default
colours pass WCAG AA contrast in both.
See src/goldpan.css for the full list.
blockedDomains) → error; a misspelling on the list still gets the fix button.gmial.com, libreo.it), and in the ending with the same name
(gmail.con, hotmail.co). A real country ending is a regional service and is left alone (hotmail.be); the
first letter must match and very short names (aol, gmx) are compared only exactly, as they sit one letter
away from many real company domains. Add your own with knownDomains.validate callback.maxEmails is reached.Comparison ignores letter case; getEmails() returns lower-cased addresses. Goldpan checks the shape of an address,
not that the mailbox exists: always validate again on the server.
All evergreen browsers (Chrome, Edge, Firefox, Safari 14+). ES2020 output.
Node.js 22.18 or newer (the current LTS is recommended).
npm install
npm run dev # builds the demo, opens it on http://localhost:5173, reloads it on every change
npm test # unit tests (Vitest + jsdom)
npm run typecheck
npm run build # dist/: ESM, CommonJS, <script> builds, types, CSS
npm run build:site # site/: the demo as published (English, Italian in site/it/, sitemap), plus site/standalone.html (works offline)
npm run lint:package
demo/index.html is the source of the demo page: opened directly, it only explains how to start it.
Goldpan is free and open source (MIT). If it saves you time, give it a star on GitHub: it helps others find it.
MIT © 2026 Michele Molinari
messages |
object |
| – |
| Replace single messages. |
placeholder | string | original input, then locale | Placeholder of the text field. |
separator | string | ', ' | Separator written into the original input. |
rejected | 'ask' | 'block' | 'skip' | 'ask' | With rejected addresses, what confirm() and a form submit do: show them and go on at the next press, stop until they are fixed, or go on without them. See below. |
inlineMessage | boolean | true | Show the message about rejected addresses inside the widget; turn it off to show it next to your button. |
editable | 'all' | 'valid' | 'invalid' | 'none' | 'all' | Rows that show the edit button. |
showCounter | boolean | true | Show the counter above the list. |
theme | 'light' | 'dark' | 'auto' | 'light' | auto follows the operating system. |
className | string | – | Extra class on the root element. |
disabled | boolean | original input | Disable the widget. |
onChange | (state) => void | – | Shortcut for on('change'). |