익스클루시브
Forgot <mv-forgot>
작성기와 폼을 위한 전송 전 “빠뜨린 것이 있을 수 있습니다” 검사기로, 막연한 경고와 달리 무엇이 빠진 것 같은지 정확히 알려 줍니다.
문화적 레퍼런스
해리 포터와 마법사의 돌, J. K. 롤링(1997, 책). 작은 유리 구슬은 주인이 무언가를 잊었을 때 흐려지지만, 무엇을 잊었는지는 알려 주지 못합니다. UI에서는 무언가 빠진 것 같을 때 전송 버튼 옆에 차분하게 빛나는 구체가 켜지며, 구슬과 달리 무엇이 빠졌는지(첨부 파일, 링크, 자리 표시자, 날짜) 정확히 알려 주고 해결 방법을 제시합니다.
작동 방식
작성기와 폼을 위한 전송 전 “빠뜨린 것이 있을 수 있습니다” 검사기로, 막연한 경고와 달리 무엇이 빠진 것 같은지 정확히 알려 줍니다. 폼을 감싸거나 for=로 폼을 지정하면, 선언적 규칙이 전송 직전의 폼을 검사합니다. 규칙이 발동하면 전송이 보류되고, 전송 버튼 옆에 차분하게 빛나는 구체가 켜지며, 작은 비모달 패널이 누락으로 보이는 항목을 하나씩 나열합니다. 각 항목에는 이를 촉발한 단어가 한 줄 발췌문 안에 강조 표시되고 고유한 해결 방법이 붙습니다. “Attach a file”은 파일 선택기를 열고, “Add the link”는 URL이 들어갈 위치에 캐럿을 두며, “Fill it in”은 남아 있는 자리 표시자를 선택하고, 사용자 정의 해결 방법은 작업 자체를 수행할 수도 있습니다(화상 회의 링크 생성 등). 해결된 항목은 실시간으로 체크되고 패널은 “All set · Send now”로 바뀝니다. “Send anyway”는 항상 원래의 제출 요소로 전송을 진행하므로 영구적으로 막는 일은 없습니다. “Ignore”는 이 메시지에 대한 알림을 무시하고, 선택 사항인 “Don’t remind me”는 규칙을 영구적으로 끕니다(브라우저에 기억되거나 mv-mute를 통해 동기화). 기본 제공 시작 규칙: 첨부 파일을 언급했지만 첨부하지 않음(영어와 흔히 쓰는 프랑스어, 독일어, 스페인어, 이탈리아어 단어), URL 없이 링크를 언급함, 자리 표시자([First name], {{company}}, TODO, TBD, lorem ipsum), 빈 제목, 날짜 필드가 비어 있는데 요일이나 시간을 언급함, 이미 지난 마감일, 너무 많은 수신자. 인용된 답장은 무시합니다. 사용자 정의 규칙은 일반 객체입니다(test(state), 또는 pattern + condition, {match}를 담은 message, fix). 라이브 모드에서는 입력하는 동안 방해하지 않고 구체가 은은하게 빛나며, 네이티브 유효성 검사 이후에 실행됩니다. 명백한 오류는 브라우저가 처리하고, 이 컴포넌트는 가볍고 그럴듯한 누락만 잡아냅니다.
| 카테고리 | 폼 |
|---|---|
| 유형 | Web Component (<mv-forgot>) |
| 상태 | 안정 |
| 키트 | 사용자를 놓치지 않는 폼 |
| 함께 설치되는 항목 | button |
| Keywords | exclusive, culture, form, pre-send, send-check, attachment-reminder, forgotten-attachment, placeholder, missing-link, omission, reminder, composer, email, linter, soft-validation, send-anyway, submit, checklist |
When to use
- An email, message or support reply composer should catch “see attached” with no attachment or a leftover [First name] before it goes out
- A form sends something hard to take back (an invoice, a meeting invite, a release note) and common omissions should be caught before submit
- A PR, ticket or changelog template mentions screenshots, links or dates that authors often forget to actually add
- Teams want a gentle, dismissible pre-send check instead of making optional fields required
Avoid when
- The value is simply invalid (required, format, range) and must be fixed before anything is sent: use native validation and field errors → use Field instead
- The user is stuck and needs step-by-step guidance toward a goal, not a check at the moment of sending → use Insist instead
- The problem is invisible or look-alike characters pasted into a field → use Invisibles instead
설치
node scripts/add.mjs forgot --out ./src/marvelousMarvelous UI MCP 서버를 사용하는 AI 에이전트: install_components({ slugs: ["forgot"], target_dir: "<absolute path>/src/marvelous", framework: "react" }).
복사되는 파일(의존성 포함): tokens/tokens.css, core/base.css, components/button/button.css, core/dismiss.js, core/dom.js, core/element.js, core/motion.js, core/position.js, components/forgot/forgot.js, components/forgot/forgot.css.
사용법
빠른 시작, 동작하는 가장 작은 마크업:
<mv-forgot>
<form>
<textarea name="body" aria-label="Message">Hi Sam, the signed contract is attached.</textarea>
<input type="file" name="files" aria-label="Attachments">
<button>Send</button>
</form>
</mv-forgot>기본 마크업입니다. 여기서 시작해 속성, data-*, CSS 변수로 커스터마이즈하세요:
<div id="fg-demo" style="width:min(100%,60rem);margin-inline:auto">
<style>
#fg-demo { display:grid; gap:1.25rem; align-content:start }
#fg-demo .fg-card { min-width:0; border:1px solid var(--mv-border); border-radius:var(--mv-radius-xl); background:var(--mv-surface); box-shadow:var(--mv-shadow-sm); overflow:hidden }
#fg-demo .fg-bar { display:flex; align-items:center; justify-content:space-between; gap:.75rem; min-height:3rem; padding:.5rem 1rem .5rem 1.125rem; border-bottom:1px solid var(--mv-border); background:var(--mv-bg-subtle) }
#fg-demo .fg-bar h3 { margin:0; font-size:.875rem; font-weight:600; letter-spacing:-.01em }
#fg-demo .fg-bar p { margin:.125rem 0 0; color:var(--mv-fg-muted); font-size:.75rem }
#fg-demo .fg-row { display:flex; align-items:center; gap:.75rem; padding:0 1.125rem; border-bottom:1px solid var(--mv-border) }
#fg-demo .fg-row label { flex:none; width:3.75rem; color:var(--mv-fg-muted); font-size:.8125rem }
#fg-demo .fg-row input { flex:1; min-width:0; height:2.625rem; padding:0; border:0; background:transparent; color:var(--mv-fg); font:inherit; font-size:.875rem; outline:none }
#fg-demo .fg-row:focus-within { box-shadow:inset 2px 0 0 var(--mv-accent) }
#fg-demo .fg-body { display:block; width:100%; box-sizing:border-box; min-height:13.5rem; padding:.875rem 1.125rem; border:0; background:transparent; color:var(--mv-fg); font:.875rem/1.6 var(--mv-font-sans); resize:vertical; outline:none }
#fg-demo .fg-body:focus-visible { box-shadow:inset 2px 0 0 var(--mv-accent) }
#fg-demo .fg-files { display:flex; flex-wrap:wrap; gap:.5rem; padding:0 1.125rem .75rem }
#fg-demo .fg-files:empty { display:none }
#fg-demo .fg-file { display:inline-flex; align-items:center; gap:.5rem; height:2rem; padding:0 .375rem 0 .625rem; border:1px solid var(--mv-border); border-radius:var(--mv-radius-md); background:var(--mv-bg-subtle); font-size:.75rem }
#fg-demo .fg-file svg { width:.875rem; height:.875rem; color:var(--mv-fg-muted) }
#fg-demo .fg-file small { color:var(--mv-fg-muted) }
#fg-demo .fg-file button { display:grid; place-items:center; width:1.375rem; height:1.375rem; padding:0; border:0; border-radius:var(--mv-radius-sm); background:none; color:var(--mv-fg-muted); cursor:pointer }
#fg-demo .fg-file button:hover { background:var(--mv-bg-muted); color:var(--mv-fg) }
#fg-demo .fg-foot { display:flex; align-items:center; gap:.5rem; flex-wrap:wrap; padding:.75rem 1rem .75rem 1.125rem; border-top:1px solid var(--mv-border) }
#fg-demo .fg-foot .fg-grow { flex:1 }
#fg-demo .fg-foot svg { width:1rem; height:1rem }
#fg-demo .fg-controls { display:flex; align-items:center; justify-content:space-between; gap:.75rem 1rem; flex-wrap:wrap; padding:.75rem 1.125rem; border-top:1px solid var(--mv-border); background:var(--mv-bg-subtle) }
#fg-demo .fg-controls .fg-actions { display:flex; align-items:center; gap:.5rem; flex-wrap:wrap }
#fg-demo .fg-log { flex:1 1 16rem; min-height:1.125rem; margin:0; color:var(--mv-fg-muted); font:.75rem/1.4 var(--mv-font-mono) }
#fg-demo .mv-choice { font-size:.8125rem }
#fg-demo .fg-pair { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:1.25rem; align-items:start }
#fg-demo .fg-form { display:grid; gap:.875rem; padding:1rem 1.125rem }
#fg-demo .fg-form .fg-two { display:grid; grid-template-columns:minmax(0,1fr) minmax(0,1fr); gap:.75rem }
#fg-demo .fg-form textarea { min-height:5.5rem; resize:vertical }
#fg-demo .fg-form .fg-foot { margin:.125rem -1.125rem -1rem; justify-content:flex-end }
#fg-demo .fg-amount { font-variant-numeric:tabular-nums }
@media (max-width:48rem) {
#fg-demo .fg-pair { grid-template-columns:minmax(0,1fr) }
}
@media (max-width:30rem) {
#fg-demo .fg-form .fg-two { grid-template-columns:minmax(0,1fr) }
}
</style>
<!-- Main: an email composer, checked live while typing and on Send -->
<section class="fg-card" aria-labelledby="fg-mail-title">
<div class="fg-bar">
<div>
<h3 id="fg-mail-title">New message</h3>
<p>Tallypoint · Customer Success</p>
</div>
<span class="mv-badge" data-variant="secondary">Draft saved 10:41 AM</span>
</div>
<mv-forgot id="fg-composer" live remember="demo-composer" max-recipients="12">
<form id="fg-mail" aria-labelledby="fg-mail-title">
<div class="fg-row">
<label for="fg-to">To</label>
<input id="fg-to" name="to" data-forgot="recipients" value="[email protected], [email protected]" autocomplete="off" spellcheck="false">
</div>
<div class="fg-row">
<label for="fg-subject">Subject</label>
<input id="fg-subject" name="subject" value="Signed SOW and next steps for the Q4 rollout" autocomplete="off">
</div>
<label class="mv-sr-only" for="fg-body">Message</label>
<textarea id="fg-body" class="fg-body" name="body" rows="9">Hi [First name],
Thanks again for the call on Tuesday. I’ve attached the signed statement of work for the Q4 rollout, along with the updated timeline.
You can review the onboarding checklist using the link below, and I’ll send the kickoff agenda by Friday. TODO: confirm the invoicing contact.
Best,
Lena Fischer
Customer Success · Tallypoint</textarea>
<div class="fg-files" id="fg-files" data-forgot="attachments" data-count="0" aria-label="Attachments"></div>
<div class="fg-foot">
<button type="button" class="mv-button" data-variant="outline" data-size="sm" id="fg-attach" data-forgot-attach>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m20.5 11.5-8.3 8.3a5.3 5.3 0 0 1-7.5-7.5l8.3-8.3a3.5 3.5 0 0 1 5 5l-8.3 8.3a1.8 1.8 0 0 1-2.5-2.5l7.6-7.6"/></svg>
Attach
</button>
<span class="fg-grow"></span>
<button type="submit" class="mv-button" data-variant="ghost" data-size="sm" data-forgot-skip>Save draft</button>
<button type="submit" class="mv-button" data-size="sm" id="fg-send">
Send
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 12 20 4l-4 16-4-7z"/><path d="m12 13 8-9"/></svg>
</button>
</div>
</form>
</mv-forgot>
<div class="fg-controls">
<p class="fg-log" id="fg-log" aria-live="off">Checked live: the glow next to Send says something may be missing.</p>
<span class="fg-actions">
<label class="mv-choice" data-control="end">
<input type="checkbox" role="switch" class="mv-switch" id="fg-live" checked>
<span class="mv-choice-text"><span class="mv-choice-title">Live check</span></span>
</label>
<button type="button" class="mv-button" data-variant="ghost" data-size="sm" id="fg-unmute">Forget muted rules</button>
<button type="button" class="mv-button" data-variant="outline" data-size="sm" id="fg-reset">Reset example</button>
</span>
</div>
</section>
<div class="fg-pair">
<!-- Variant: checked only when submitting, with a custom rule and its own verb -->
<section class="fg-card" aria-labelledby="fg-meet-title">
<div class="fg-bar">
<div>
<h3 id="fg-meet-title">New meeting</h3>
<p>Checked when you press Schedule</p>
</div>
</div>
<mv-forgot id="fg-meeting" verb="schedule" builtins="date, placeholder">
<form class="fg-form" id="fg-meet" aria-labelledby="fg-meet-title">
<div class="mv-field">
<label class="mv-label" for="fg-m-title">Title</label>
<input class="mv-input" id="fg-m-title" name="title" value="Q4 rollout walkthrough" required data-forgot-scan>
</div>
<div class="fg-two">
<div class="mv-field">
<label class="mv-label" for="fg-m-when">Date and time</label>
<input class="mv-input" id="fg-m-when" name="when" type="datetime-local" data-forgot="date">
</div>
<div class="mv-field">
<label class="mv-label" for="fg-m-where">Video link</label>
<input class="mv-input" id="fg-m-where" name="location" type="url" placeholder="https://" autocomplete="off">
</div>
</div>
<div class="mv-field">
<label class="mv-label" for="fg-m-desc">Description</label>
<textarea class="mv-textarea" id="fg-m-desc" name="description" rows="3">Let’s meet tomorrow at 3 pm to walk through the Q4 rollout plan with the Acme team. We’ll join online and share the timeline on screen.</textarea>
</div>
<div class="fg-foot">
<button type="submit" class="mv-button" data-size="sm">Schedule</button>
</div>
</form>
</mv-forgot>
</section>
<!-- Variant: live, only the placeholder and past-date rules -->
<section class="fg-card" aria-labelledby="fg-inv-title">
<div class="fg-bar">
<div>
<h3 id="fg-inv-title">Invoice INV-2048</h3>
<p>Acme Trading · <span class="fg-amount">$17,400.00</span></p>
</div>
</div>
<mv-forgot id="fg-invoice" live builtins="placeholder, past-date">
<form class="fg-form" id="fg-inv" aria-labelledby="fg-inv-title">
<div class="fg-two">
<div class="mv-field">
<label class="mv-label" for="fg-i-to">Bill to</label>
<input class="mv-input" id="fg-i-to" name="client" value="[email protected]" autocomplete="off">
</div>
<div class="mv-field">
<label class="mv-label" for="fg-i-due">Due date</label>
<input class="mv-input" id="fg-i-due" name="due" type="date" data-forgot="due">
</div>
</div>
<div class="mv-field">
<label class="mv-label" for="fg-i-note">Message</label>
<textarea class="mv-textarea" id="fg-i-note" name="note" rows="3">Hi {{client_first_name}}, here is invoice INV-2048 for the Q4 rollout ($17,400.00). Thank you for your business!</textarea>
</div>
<div class="fg-foot">
<button type="submit" class="mv-button" data-size="sm">Send invoice</button>
</div>
</form>
</mv-forgot>
</section>
</div>
<script type="module">
const composer = document.getElementById("fg-composer");
const mail = document.getElementById("fg-mail");
const files = document.getElementById("fg-files");
const log = document.getElementById("fg-log");
const meeting = document.getElementById("fg-meeting");
const invoice = document.getElementById("fg-invoice");
const time = () => new Date().toLocaleTimeString("en-US", { hour: "numeric", minute: "2-digit" });
const say = (text) => { log.textContent = `${time()} · ${text}`; };
// A simulated uploader: one click attaches the signed PDF.
const clip = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M14 3.5H7a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V8.5z"/><path d="M14 3.5v5h5"/></svg>';
const sync = () => {
files.dataset.count = String(files.children.length);
files.dispatchEvent(new Event("change", { bubbles: true }));
};
document.getElementById("fg-attach").addEventListener("click", () => {
if (files.children.length) return;
const chip = document.createElement("span");
chip.className = "fg-file";
chip.innerHTML = `${clip}<span>SOW-Acme-signed.pdf</span><small>284 KB</small>`;
const remove = document.createElement("button");
remove.type = "button";
remove.setAttribute("aria-label", "Remove SOW-Acme-signed.pdf");
remove.innerHTML = '<svg viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><path d="M6 6l12 12M18 6 6 18"/></svg>';
remove.addEventListener("click", () => { chip.remove(); sync(); });
chip.append(remove);
files.append(chip);
sync();
});
// What the app sees.
composer.addEventListener("mv-forgot", (e) => {
const ids = e.detail.findings.map((f) => f.id).join(", ");
say(e.detail.trigger === "submit" ? `Send held: ${ids}.` : `Checked: ${ids}.`);
});
composer.addEventListener("mv-send-anyway", (e) => say(`Sent anyway despite ${e.detail.findings.length} reminder${e.detail.findings.length === 1 ? "" : "s"}.`));
composer.addEventListener("mv-mute", (e) => say(`Won’t remind about “${e.detail.id}” again (saved in this browser).`));
mail.addEventListener("submit", (e) => {
e.preventDefault();
if (e.submitter?.hasAttribute("data-forgot-skip")) { say("Draft saved, no check needed."); return; }
say("Message sent to 2 recipients.");
});
document.getElementById("fg-live").addEventListener("change", (e) => { composer.live = e.target.checked; });
document.getElementById("fg-unmute").addEventListener("click", () => { composer.unmute(); say("Muted rules forgotten: every reminder is back."); });
document.getElementById("fg-reset").addEventListener("click", () => {
mail.reset();
files.replaceChildren();
sync();
say("Example restored.");
});
// A custom rule: the invite says people join online, but there is no video link.
meeting.rules = [{
id: "video-link",
icon: "video",
pattern: /\b(?:join(?:ing)? online|video call|meet online|remotely)\b/i,
condition: (state) => !String(state.get("location")).trim(),
message: "The description says “{match}”, but there’s no video link.",
fix: {
label: "Create a video link",
run: (ctx) => {
const field = ctx.state.field("location");
field.value = "https://meet.example.com/q4-rollout-8fk2";
field.dispatchEvent(new Event("input", { bubbles: true }));
},
},
}];
document.getElementById("fg-meet").addEventListener("submit", (e) => { e.preventDefault(); say("Meeting scheduled."); });
document.getElementById("fg-inv").addEventListener("submit", (e) => { e.preventDefault(); say("Invoice INV-2048 sent."); });
// The invoice due date is nine days ago, whatever today is.
const due = document.getElementById("fg-i-due");
const past = new Date(Date.now() - 9 * 864e5);
due.value = due.defaultValue = `${past.getFullYear()}-${String(past.getMonth() + 1).padStart(2, "0")}-${String(past.getDate()).padStart(2, "0")}`;
due.dispatchEvent(new Event("change", { bubbles: true }));
// Show the mechanic right away: what the composer would say on Send.
customElements.whenDefined("mv-forgot").then(() => requestAnimationFrame(() => composer.check()));
</script>
</div>API
Attributes
| Name | 유형 | Default | Description |
|---|---|---|---|
live | boolean | false | Also evaluate while typing (debounced by delay): the orb glows softly next to the send button with the number of likely omissions and the send button's description mentions it, without opening anything or announcing. Without it, rules only run when sending (or through check()). |
builtins | "all" | "none" | list | all | Built-in rules to enable, comma or space separated: attachment, link, placeholder, subject, date, past-date, recipients. Each one only applies when the fields it needs exist (a date field for date, data-forgot="recipients" for recipients…). |
remember | string | Opt-in storage key: shows “Don’t remind me” on each finding and keeps muted rule ids in localStorage under mv-forgot:<key>. Without it, reminders can only be ignored for the current message. | |
send | CSS selector | Which submit button(s) are checked, inside the form. Default: every submitter except those with data-forgot-skip (e.g. Save draft); the first submitter anchors the orb and the panel. Without a form, the buttons whose clicks are held (default [data-forgot-send]). | |
for | id | Checks the form with this id instead of the form inside (or around) the element. | |
verb | string | send | Action word used in texts: “Before you schedule”, “Schedule anyway”, “Schedule now”. |
placement | top-end | top-start | top | bottom-end | bottom-start | bottom | top-end | Preferred side of the panel relative to the send button (flips and shifts to stay in the viewport). |
indicator | auto | none | auto | auto inserts the orb right before the send button, or inside your own [data-forgot-indicator] element if there is one (framework-friendly); none shows no orb, only the panel. |
max-recipients | number | 10 | Above this many addresses in the data-forgot="recipients" fields, the recipients rule fires (“This will go to 42 people.”). |
delay | number (ms) | 450 | Debounce of the live evaluation after the last keystroke. While a send is held, fixes are re-checked after 160 ms. |
data-forgot | body | subject | attachments | link | date | due | recipients | On a field, gives it a role for the built-in rules. Defaults without it: the first textarea or contenteditable is the body, name="subject" the subject, input[type=url] the link field, date / datetime-local inputs the date, every input[type=file] counts as attachments. On a custom uploader, data-forgot="attachments" with data-count="n" (dispatch a change event when it changes). | |
data-forgot-attach / data-forgot-future / data-forgot-scan / data-forgot-skip / data-forgot-send | marker attributes | data-forgot-attach: the button the “Attach a file” fix clicks (otherwise the file input). data-forgot-future: a date field that must not be in the past (like data-forgot="due"). data-forgot-scan: another text field whose text the rules read. data-forgot-skip: a submitter that is never checked. data-forgot-send: the send buttons of a region without a form. | |
data-state / data-count / data-open / data-clear | set by the component | On the host: idle | live | flagged | clear, the number of likely omissions, while the panel is open, during the short “All set” after everything was fixed. |
Properties
| Name | 유형 | Description |
|---|---|---|
rules | Array<{ id, test?(state), pattern?, in?, condition?(state, match), message, field?, fix?, icon? }> | Custom rules, added to the built-ins (same id replaces one). test(state) returns falsy, true, a message string or { message?, match?, field?, vars?, fix? }; or give pattern (RegExp or word) + condition(state, match) returning true when something is missing. message may use {match} and your vars, or be a function (state, match). fix: { label, run(ctx) } or { label, field } (focus) or { label, click } (selector). icon: clip, link, brackets, subject, calendar, clock, people, video, image, dot. state: { form, root, fields, subject, body, text, attachments, hasLink, recipients, today, get(name), field(name), find(pattern, where?), findAll(pattern, where?) }. |
strings | Partial<Record<string, string>> | Overrides every visible text, announcement and built-in rule message ({verb} / {Verb} / {count} / {s} / {match} placeholders). Keys: title, textOne, textOther, allSetTitle, allSetText, sendAnyway, send, keepEditing, ignore, mute, showMe, resolved, indicatorFlagged, indicatorClear, indicatorName, indicatorClearName, sendHint, announce, announceCheck, announceAllSet, announceIgnored, announceMuted, attachment, attachmentFix, link, linkFix, placeholder, placeholderMany, placeholderFix, subject, subjectFix, date, dateFix, pastDate, pastDateFix, recipients, recipientsFix. |
muted | string[] | Rule ids the user asked not to be reminded about. Read it, or set it from your server's preferences (persisted with remember). |
findings | Array<{ id, message, match, field }> | Current likely omissions, ignored and muted rules excluded (read-only). |
target | HTMLFormElement | HTMLElement | The form (or region) being checked (read-only). |
isOpen | boolean | True while the panel is open (read-only). |
Methods
| Name | Description |
|---|---|
check({ open? }) | Checks now, like a send would (emits the cancelable mv-forgot, flags the orb, opens the panel unless open: false) and returns the findings. For composers without a form: if (forgot.check().length) return; then send on mv-send-anyway. |
evaluate() | Runs every rule without any UI or event and returns the findings. |
sendAnyway() | Proceeds despite the findings: emits mv-send-anyway, then re-submits with the original submitter (requestSubmit, so native validation still applies) or clicks the send button. |
open() / close() | Opens the panel on the held findings, or folds it back (focus returns to the send button when it was inside). |
unmute(id?) | Reminds again about one rule, or every muted rule without an argument. |
Events
| Name | Description |
|---|---|
mv-forgot | Cancelable, before a send is held. detail: { findings: [{ id, message, match, field }], trigger: "submit" | "click" | "check" | "indicator" }. preventDefault() lets the send go through untouched (e.g. an internal recipient). |
mv-send-anyway | “Send anyway” was chosen while reminders were still open, just before the form is re-submitted. detail: { findings } (the ones overridden, for analytics). |
mv-forgot-change | The live findings changed (live mode, or while a send is held). detail: { findings }. |
mv-mute | “Don’t remind me” was chosen. detail: { id, muted } (all muted ids), to sync the preference to your server. |
Content structure
| Name | Description |
|---|---|
(content) | The form to check (or any region with fields and data-forgot-send buttons). The orb is inserted before the send button (or into [data-forgot-indicator]), the panel right after it so Tab reaches it next; both leave with the element. |
CSS classes
| Name | Description |
|---|---|
mv-forgot-indicator | The orb button (<button>, aria-expanded): .mv-forgot-orb (with .mv-forgot-ripple), .mv-forgot-tick, .mv-forgot-count, .mv-forgot-label; data-state="live | flagged | clear". |
mv-forgot-panel | Non-modal dialog (Popover API top layer): .mv-forgot-head (-head-orb, -title, -text), .mv-forgot-list of .mv-forgot-item (data-rule, data-resolved) with -item-icon, -msg, -quote (.mv-forgot-mark), -item-actions (.mv-forgot-fix, .mv-forgot-link), and .mv-forgot-actions (.mv-forgot-keep, .mv-forgot-go). data-all-set once everything is fixed. |
CSS variables
| Name | Default | Description |
|---|---|---|
--mv-forgot-glow | var(--mv-warning) | Color of the orb, its glow, the finding icons and the highlighted words. |
--mv-forgot-clear | var(--mv-success) | Color of the “All set” state. |
Accessibility
Nothing is ever blocked for good and nothing relies on color. When a send is held, focus stays on the send button and an assertive live region says what may be missing (“Before you send, check 3 things. You wrote “attached”, but nothing is attached. … Choose Send anyway to send as is.”); on-demand checks are announced politely and live mode never announces, it only adds a visually hidden description to the send button (“3 things may be missing before you send.”) so screen reader users hear it when they reach Send. The panel is a labelled, described non-modal dialog inserted right after the send button in the DOM, so Tab goes from Send straight to the first finding; every finding offers real buttons (its fix, “Ignore”, and “Don’t remind me” when remember is set) described by the finding's message, and the excerpt shows the triggering words in a <mark>. Fixes that select text or focus a field move focus there and fold the panel; a fix that opens a picker keeps it open. Resolved findings get a check icon and the word “Fixed”, and focus moves on to the next one; Escape or an outside click folds the panel and focus returns to the send button when it was inside, and moving focus past the checked form (Tab out of the panel) folds it without moving focus, so it never covers what comes next; “Keep editing” goes to the first omission. The orb is a <button> with aria-expanded whose accessible name includes its visible count (“3 possible omissions before you send. Review”); it states its meaning with a number and a label, not a color. Runs after native constraint validation, so real errors keep the browser's own messages. Reduced motion (OS or data-motion="reduce"): the glow is static, no swell or ripple, the panel appears without scaling. Forced colors: the orb and highlights use system colors (Highlight, Mark).