Native Shell
Native Shell:AI 生成的 UI,在你的应用中原生呈现,覆盖每个平台。iOS、Android、macOS、Windows 与 Linux 上的应用、GPUI 应用与网页在运行时加载已签名、沙箱隔离的 TSX 模块并绘制 th
AI 翻译
个人 GitHub 账号下公开仓库的所有者可以登录并验证所有权,自行管理介绍,无需管理员批准。
登录并生成验证文件,在 30 分钟内将其提交到此仓库的默认分支。其他分支或分叉仓库中的文件无效。然后在下方检查文件,即可无需管理员批准直接管理项目。请保留文件以维持管理权限。
如需登录,将前往登录页面。登录后请回到此处,再次点击所选的验证按钮。
在项目文件夹中运行 /nomorevibe verify,即可验证所有权并自行管理介绍。
一分钟完成验证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'). |