익스클루시브
Same Again <mv-same-again>
이전 회차를 기억하는 반복 폼입니다. 데일리 스탠드업, 근무 시간표, 교대 인수인계, 체크리스트, 주간 보고서를 감싸면 제출할 때마다 답변이 날짜와 함께 저장됩니다(키 아래 브라우저 저장소, 또는 previous 속성과 mv-loop-saved를 통한 자체 서버).
문화적 레퍼런스
사랑의 블랙홀, 해럴드 래미스 (1993년, 영화). 한 남자가 매일 같은 날에 다시 깨어나고, 회차마다 이전 회차에서 배운 모든 것을 다시 활용하므로 반복되는 하루가 점점 더 빠르고 나아집니다. UI에서는 반복 폼이 이전 회차를 기억해 필드별로 각 답변을 다시 제안하며, 이월된 값은 사용자가 확인할 때까지 눈에 띄게 표시됩니다.
작동 방식
이전 회차를 기억하는 반복 폼: 데일리 스탠드업, 근무 시간표, 교대 인수인계, 체크리스트, 주간 보고서를 감싸면 제출할 때마다 답변이 날짜와 함께 저장됩니다(키 아래의 브라우저 저장소, 또는 previous 속성과 mv-loop-saved를 통한 서버). 다음번에는 각 필드가 입력하는 바로 그 자리에서 마지막 답변을 제안합니다: 빈 텍스트 필드 안의 색이 입혀진 고스트 플레이스홀더, 그리고 모든 필드 아래의 작은 칩(“Yesterday · Remote · 5 in a row”)입니다. 빈 필드에서 →(tab-accepts가 있으면 Tab)를 누르거나 클릭하면 재사용하고, Escape로 닫습니다. 바 하나가 회차를 요약하며(“Loop 42 · Last filled yesterday at 9:12 AM”, 7개 기간의 점 띠, “14-day streak”) “Same as yesterday”를 제공합니다. 이 기능은 사용자가 건드리지 않은 필드만 채우고, 각 필드를 확인하거나 수정할 때까지 “Carried over from yesterday · Confirm · Undo”로 표시하므로 아무도 모르는 사이에 오래된 답변을 제출하지 않습니다. require-review는 그때까지 제출을 막습니다. 최근 회차마다 답이 바뀐 필드는 제안하지 않고, data-same-again="never" 아래의 필드(그리고 날짜, 비밀번호, 카드 데이터, 일회용 코드, hidden 및 file 입력)는 절대 저장하지 않습니다. cadence(daily, weekdays, weekly, none)는 연속 기록을 결정하며, 같은 기간에 다시 제출하면 새 회차로 세지 않고 기존 회차를 대체합니다. 모든 재사용은 앱이 값을 다시 쓸 수 있는 취소 가능한 mv-carry를 거칩니다. 네이티브 폼, 라디오 및 체크박스 그룹, 다중 select, 스위치, form-associated 커스텀 요소를 지원하며, 값은 input/change 이벤트와 함께 네이티브 setter로 기록되므로 프레임워크 상태가 계속 동기화됩니다.
| 카테고리 | 폼 |
|---|---|
| 유형 | Web Component (<mv-same-again>) |
| 상태 | 안정 |
| 키트 | 사용자를 놓치지 않는 폼 |
| 함께 설치되는 항목 | button |
| Keywords | exclusive, culture, form, recurring, repeat, prefill, autofill, previous-answers, standup, timesheet, handover, checklist, check-in, weekly-report, streak, suggestion, ghost-text, carry-over, review, form-state |
When to use
- People fill the same form every day or week (standup, timesheet, habit or health check-in) and most answers rarely change
- A shift handover or site log should start from the previous shift’s answers without anyone submitting them unread
- A weekly report or expense claim repeats last period’s structure and only a few fields need new values
- Compliance requires that reused answers be confirmed one by one before a recurring form is sent
Avoid when
- The goal is to recover an unfinished draft after a crash or reload, not to reuse a submitted one → use Bloodstain instead
- Users need to browse or restore earlier versions of the same form while editing it → use Rewind instead
- Answers must be re-attested from scratch every time (safety checks, clinical readings): mark them data-same-again="never"
설치
node scripts/add.mjs same-again --out ./src/marvelousMarvelous UI MCP 서버를 사용하는 AI 에이전트: install_components({ slugs: ["same-again"], target_dir: "<absolute path>/src/marvelous", framework: "react" }).
복사되는 파일(의존성 포함): tokens/tokens.css, core/base.css, components/button/button.css, core/dom.js, core/element.js, core/motion.js, components/same-again/same-again.js, components/same-again/same-again.css.
사용법
빠른 시작, 동작하는 가장 작은 마크업:
<mv-same-again storage-key="standup" cadence="weekdays">
<form>
<label>Working from <select name="location"><option>Office</option><option>Remote</option></select></label>
<label>Blockers <input name="blockers"></label>
<button>Submit</button>
</form>
</mv-same-again>기본 마크업입니다. 여기서 시작해 속성, data-*, CSS 변수로 커스터마이즈하세요:
<div id="sa-demo" style="width:min(100%,64rem);margin-inline:auto">
<style>
#sa-demo { display:grid; gap:1.25rem }
#sa-demo .sa-layout { display:grid; grid-template-columns:minmax(0,1fr) 16.5rem; gap:1.25rem; align-items:start }
#sa-demo .sa-card { min-width:0; padding:1.25rem 1.375rem 1.375rem; border:1px solid var(--mv-border); border-radius:var(--mv-radius-xl); background:var(--mv-surface); box-shadow:var(--mv-shadow-sm) }
#sa-demo .sa-head { display:flex; align-items:flex-start; justify-content:space-between; gap:.75rem; margin:0 0 1rem }
#sa-demo .sa-head h3 { margin:0; font-size:1.0625rem; letter-spacing:-.01em }
#sa-demo .sa-head p { margin:.1875rem 0 0; color:var(--mv-fg-muted); font-size:.8125rem }
#sa-demo .sa-radios { display:flex; gap:.5rem 1.25rem; flex-wrap:wrap; padding-top:.25rem }
#sa-demo .sa-checks { display:grid; gap:.625rem; justify-items:start }
#sa-demo .sa-foot { display:flex; align-items:center; justify-content:space-between; gap:.75rem; flex-wrap:wrap; margin-top:1.25rem; padding-top:1rem; border-top:1px solid var(--mv-border) }
#sa-demo .sa-foot small { color:var(--mv-fg-muted); font-size:.75rem }
#sa-demo .sa-side { display:grid; gap:1rem; position:sticky; top:1rem }
#sa-demo .sa-panel { padding:1rem 1.125rem; border:1px solid var(--mv-border); border-radius:var(--mv-radius-xl); background:var(--mv-bg-subtle) }
#sa-demo .sa-panel h4 { margin:0 0 .625rem; font-size:.8125rem }
#sa-demo .sa-steps { margin:0 0 1rem; padding-inline-start:1.125rem; color:var(--mv-fg-muted); font-size:.8125rem; line-height:1.5; display:grid; gap:.375rem }
#sa-demo .sa-steps b { color:var(--mv-fg); font-weight:600 }
#sa-demo .sa-next { width:100% }
#sa-demo .sa-panel .mv-choice { font-size:.8125rem; margin-top:.875rem }
#sa-demo .sa-log { display:grid; gap:.375rem; margin:0; padding:0; list-style:none; font:.75rem/1.4 var(--mv-font-mono); color:var(--mv-fg-muted) }
#sa-demo .sa-log li { overflow-wrap:anywhere }
#sa-demo .sa-log b { color:var(--mv-fg); font-weight:600 }
#sa-demo .sa-ho-grid { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:1rem 1.25rem; align-items:start }
#sa-demo .sa-ho-grid .sa-wide { grid-column:1 / -1 }
@media (max-width:54rem) {
#sa-demo .sa-layout { grid-template-columns:minmax(0,1fr) }
#sa-demo .sa-side { position:static }
}
@media (max-width:36rem) { #sa-demo .sa-ho-grid { grid-template-columns:minmax(0,1fr) } }
</style>
<div class="sa-layout">
<!-- Main: an async daily standup, weekdays cadence, history from the "server" -->
<section class="sa-card" aria-labelledby="sa-title">
<div class="sa-head">
<div>
<h3 id="sa-title">Daily check-in</h3>
<p>Payments squad · async standup, due by 10:00 AM</p>
</div>
<span class="mv-badge" data-variant="secondary">Weekdays</span>
</div>
<mv-same-again id="sa-main" cadence="weekdays" storage="none">
<form id="sa-form" novalidate>
<div class="mv-field-group">
<div class="mv-field-row">
<div class="mv-field">
<label class="mv-label" for="sa-date">Date</label>
<input class="mv-input" type="date" id="sa-date" name="date">
</div>
<div class="mv-field">
<label class="mv-label" for="sa-project">Main project</label>
<select class="mv-select" id="sa-project" name="project">
<option value="">Choose a project…</option>
<option value="checkout">Checkout redesign</option>
<option value="retries">Payment retries</option>
<option value="fraud">Fraud rules v2</option>
<option value="tools">Internal tools</option>
</select>
</div>
</div>
<fieldset class="mv-field" style="border:0;margin:0;padding:0;min-width:0">
<legend class="mv-label">Working from</legend>
<div class="sa-radios">
<label class="mv-choice"><input type="radio" class="mv-radio" name="location" value="remote"> Remote</label>
<label class="mv-choice"><input type="radio" class="mv-radio" name="location" value="office"> Office</label>
<label class="mv-choice"><input type="radio" class="mv-radio" name="location" value="client"> Client site</label>
</div>
</fieldset>
<div class="mv-field">
<label class="mv-label" for="sa-done">Since the last check-in</label>
<textarea class="mv-textarea" id="sa-done" name="done" rows="2" placeholder="What you shipped, reviewed or learned"></textarea>
</div>
<div class="mv-field-row">
<div class="mv-field">
<label class="mv-label" for="sa-ticket">Focus ticket</label>
<input class="mv-input" id="sa-ticket" name="ticket" placeholder="PAY-000 · Short title">
</div>
<div class="mv-field">
<label class="mv-label" for="sa-blockers">Blockers</label>
<input class="mv-input" id="sa-blockers" name="blockers" placeholder="Anything slowing you down?">
</div>
</div>
<div class="sa-checks">
<label class="mv-choice"><input type="checkbox" class="mv-switch" role="switch" name="pairing"> Open to pairing today</label>
<label class="mv-choice" data-same-again="never"><input type="checkbox" class="mv-checkbox" name="resolved"> A blocker was resolved since the last check-in</label>
</div>
</div>
<div class="sa-foot">
<small>In an empty field, press <kbd class="mv-kbd" data-size="sm">→</kbd> to reuse the last answer.</small>
<button type="submit" class="mv-button">Submit check-in</button>
</div>
</form>
</mv-same-again>
</section>
<aside class="sa-side" aria-label="Try it">
<div class="sa-panel">
<h4>Try it</h4>
<ol class="sa-steps">
<li>Press <b>→</b> in an empty field, or click a suggestion under it.</li>
<li><b>Same as yesterday</b> fills only the fields you haven’t touched, and marks them until you confirm or edit them.</li>
<li>Submit, then <b>start tomorrow</b>: today’s answers become the next suggestions.</li>
</ol>
<button type="button" class="mv-button sa-next" data-variant="outline" id="sa-next" disabled>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 12h14"/><path d="m13 6 6 6-6 6"/></svg>
Start tomorrow’s check-in
</button>
<label class="mv-choice"><input type="checkbox" class="mv-switch" role="switch" data-size="sm" id="sa-review"> Require review before submit</label>
</div>
<div class="sa-panel">
<h4>Events</h4>
<ul class="sa-log" id="sa-log" aria-live="polite"><li>“Since the last check-in” changed every day, so it gets no suggestion.</li></ul>
</div>
</aside>
</div>
<!-- Variant: shift handover, one loop per submission, carried at load, review required -->
<section class="sa-card" aria-labelledby="sa-ho-title">
<div class="sa-head">
<div>
<h3 id="sa-ho-title">Shift handover · Packaging line 3</h3>
<p>Plant 2, Rotterdam · filled at the end of every shift</p>
</div>
<span class="mv-badge" data-variant="warning">Review required</span>
</div>
<mv-same-again id="sa-ho" cadence="none" storage="none" require-review>
<form id="sa-ho-form" novalidate>
<div class="sa-ho-grid">
<div class="mv-field">
<label class="mv-label" for="sa-ho-status">Line status</label>
<select class="mv-select" id="sa-ho-status" name="status">
<option value="">Choose…</option>
<option value="running">Running normally</option>
<option value="issues">Running with issues</option>
<option value="stopped">Stopped</option>
</select>
</div>
<div class="mv-field">
<label class="mv-label" for="sa-ho-next">Handing over to</label>
<input class="mv-input" id="sa-ho-next" name="next" placeholder="Name of the incoming lead">
</div>
<div class="mv-field sa-wide">
<label class="mv-label" for="sa-ho-issues">Open issues</label>
<textarea class="mv-textarea" id="sa-ho-issues" name="issues" rows="2"></textarea>
</div>
<div class="mv-field">
<label class="mv-label" for="sa-ho-units">Cases packed this shift</label>
<input class="mv-input" type="number" id="sa-ho-units" name="units" inputmode="numeric">
</div>
<div class="mv-field" style="align-self:end" data-same-again="never">
<label class="mv-choice"><input type="checkbox" class="mv-checkbox" name="safety"> Safety walk done this shift</label>
</div>
</div>
<div class="sa-foot">
<small>Carried-over answers must be confirmed or edited before the handover can be sent.</small>
<button type="submit" class="mv-button" id="sa-ho-submit">Submit handover</button>
</div>
</form>
</mv-same-again>
</section>
<script type="module">
await customElements.whenDefined("mv-same-again");
const main = document.getElementById("sa-main");
const form = document.getElementById("sa-form");
const ho = document.getElementById("sa-ho");
const log = document.getElementById("sa-log");
const next = document.getElementById("sa-next");
const review = document.getElementById("sa-review");
const say = (html) => {
const li = document.createElement("li");
li.textContent = html;
log.prepend(li);
while (log.children.length > 5) log.lastElementChild.remove();
};
const at = (d, h, m) => { const x = new Date(d); x.setHours(h, m, 0, 0); return x.getTime(); };
const prevWeekday = (t) => { const d = new Date(t); do d.setDate(d.getDate() - 1); while (d.getDay() === 0 || d.getDay() === 6); return d.getTime(); };
const today = () => { const d = new Date(); return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`; };
// Fourteen weekdays of history, as a server would return them (oldest first).
const DONE = [
"Kicked off the checkout copy review with Design.", "Fixed the currency rounding bug in receipts.", "Shipped the new address form to 5% of traffic.",
"Paired with Amara on the tax line breakdown.", "Wrote the rollout note for the checkout redesign.", "Closed the last checkout copy tickets.",
"Mapped every webhook the replay tool must cover.", "Built the replay queue and its dead-letter view.", "Reviewed PR #398 and added replay metrics.",
"Shipped webhook replay to 10% of merchants.", "Handled two replay escalations from support.", "Wrote the retry backoff RFC and shared it.",
"Paired with Kenji on idempotency keys; merged PR #409.", "Reviewed PR #412 and fixed the flaky refund test.",
];
let t = at(prevWeekday(Date.now()), 9, 12);
const loops = [];
for (let i = 13; i >= 0; i--) {
loops.unshift({ t, values: {
project: i >= 6 ? "retries" : "checkout",
location: i % 4 === 2 && i < 11 ? "office" : "remote",
done: DONE[i],
ticket: i >= 11 ? "PAY-231 · Retry backoff" : i >= 6 ? "PAY-219 · Webhook replay" : "PAY-188 · Checkout copy",
blockers: i >= 12 ? "Waiting on sandbox credentials from the Risk team" : "None",
pairing: i % 3 !== 0,
} });
t = at(prevWeekday(t), 9, [4, 9, 12, 7, 15, 2, 11][i % 7]);
}
main.previous = { count: 41, loops };
// Today, before anyone clicked: the date is set, and two answers were already typed.
document.getElementById("sa-date").value = today();
const done = document.getElementById("sa-done");
done.value = "Merged the retry backoff (PR #415) and updated the runbook.";
done.dispatchEvent(new Event("input", { bubbles: true }));
const office = form.querySelector('input[name="location"][value="office"]');
office.checked = true;
office.dispatchEvent(new Event("change", { bubbles: true }));
main.addEventListener("mv-carry", (e) => say(`mv-carry · ${e.detail.source} · ${e.detail.fields.join(", ")}`));
main.addEventListener("mv-loop-saved", (e) => {
say(`mv-loop-saved · loop ${e.detail.loop} · ${e.detail.streak}-day streak`);
next.disabled = false;
});
main.addEventListener("mv-review-required", (e) => say(`mv-review-required · ${e.detail.fields.join(", ")}`));
form.addEventListener("submit", (e) => e.preventDefault()); // a fetch() would go here
// Simulated tomorrow: every stored loop moves one weekday back, the form starts empty.
next.addEventListener("click", () => {
const p = main.previous;
main.previous = { count: p.count, loops: p.loops.map((l) => ({ ...l, t: prevWeekday(l.t) })), dates: p.dates.map(prevWeekday) };
form.reset();
document.getElementById("sa-date").value = today();
next.disabled = true;
say(`New day · loop ${main.loop}`);
});
review.addEventListener("change", () => { main.requireReview = review.checked; });
// Handover: one loop per shift, the unchanged answers carried as soon as the form opens.
const shift = (daysAgo, h, m) => at(Date.now() - daysAgo * 864e5, h, m);
ho.previous = { count: 212, loops: [
{ t: shift(1, 14, 3), values: { status: "issues", next: "Tomás Herrera", issues: "Label printer jams about every 2 hours. Spare rolls are in cabinet B.", units: "1840" } },
{ t: shift(1, 22, 1), values: { status: "issues", next: "Yuki Tanaka", issues: "Label printer jams about every 2 hours. Spare rolls are in cabinet B.", units: "1712" } },
{ t: shift(0, 6, 2), values: { status: "issues", next: "Ines Duarte", issues: "Label printer jams about every 2 hours. Spare rolls are in cabinet B. Maintenance booked for Thursday.", units: "1655" } },
] };
ho.carry();
document.getElementById("sa-ho-form").addEventListener("submit", (e) => e.preventDefault());
</script>
</div>API
Attributes
| Name | 유형 | Default | Description |
|---|---|---|---|
storage-key | string | <pathname>:<form id | name | action> | Storage key of the loop history (stored as mv-same-again:<storage-key>). Set it per form and per person when several recurring forms share a URL. |
storage | local | session | none | local | Where loops are kept in the browser. none keeps nothing locally: feed the history with the previous property and persist it from mv-loop-saved. |
cadence | daily | weekdays | weekly | none | daily | Rhythm of the loop. A resubmission in the same period (day, ISO week) updates the last loop instead of counting a new one; the streak counts consecutive periods (weekdays skips Saturday and Sunday). none: every submission is a new loop and no streak is shown (shift handovers). |
keep | number | 10 | How many loops are retained (the loop counter keeps counting beyond it). |
volatile-after | number | 3 | A field whose last N answers are all different is considered volatile and gets no suggestion (a “what I did yesterday” field). 0 or 1 disables the rule; data-same-again="always" overrides it per field. |
max-age | time ("30d", "2w", "12h", ms number) | Beyond this age the last loop is too old to suggest: the bar says so and no field is offered. No limit by default. | |
require-review | boolean | Submit is blocked (preventDefault + stopImmediatePropagation in the capture phase, before your handlers) while carried-over fields are unconfirmed: the bar turns into a review message listing them, focus moves to the first one and mv-review-required fires. For fetch flows outside a <form>, call checkReview(). | |
tab-accepts | boolean | Tab also reuses the suggestion of an empty text field (focus stays; the next Tab moves on). Off by default so Tab always navigates. | |
suggest | inline | chip | inline | inline shows the previous answer as a tinted placeholder inside empty text fields (the chip then reads “Same as yesterday →”); chip keeps the author’s placeholder and shows the value in the chip only. |
save-on | submit | manual | submit | submit stores a loop on every submit that the review gate lets through (even if your handler calls preventDefault for a fetch). manual: call saveLoop() once your server has accepted the answers. |
locale | BCP 47 tag | en-US | Locale of weekdays, dates and times (“Friday”, “Sep 12”, “9:12 AM”). |
data-same-again (on a field or ancestor) | never | always | manual | never: not stored and never suggested (dates, “blocker resolved”, signatures). always: suggested even when volatile, and allowed on date/time inputs (excluded by default). manual: suggested field by field but left out of “Same as yesterday”. | |
data-same-again-bar (on a descendant) | boolean | An element that receives the loop bar (a card header…) instead of the top of the component. | |
data-same-again-state (set on controls) | suggest | carried | volatile | Styling hook written by the component on each control. data-same-again-ghost is set while the placeholder shows the previous answer. |
Properties
| Name | 유형 | Description |
|---|---|---|
previous | { t, values, loop? } | Array<{ t, values, loop? }> | { count, loops, dates? } | History from your server (t as ms, Date or ISO string; values keyed by field name in the shapes below; dates optionally lists older submission times so the streak can outlive the loops you send). Takes precedence over storage and is updated in memory by saveLoop(). Value shapes: string for text and single selects, string | null for radio groups, boolean for a lone checkbox or switch, string[] for checkbox groups and multiple selects, the element’s value for form-associated custom elements. |
loops | Array<{ t, values, loop? }> | Stored loops, oldest first (read-only copy). |
loop | number | Number of the loop being filled now: the last loop’s number when it belongs to the current period, the next one otherwise (read-only). |
streak | number | Consecutive periods with a loop, ending with the current period or the previous one (read-only). |
carried | string[] | Names of the carried-over fields not confirmed yet (read-only). |
strings | Partial<Record<string, string>> | Overrides for every visible text and announcement (loop, first, last, again, stale, saved, updated, streak, day, week, today, yesterday, same, sameLabel, chipLabel, run, carried, confirm, confirmLabel, undoField, undoFieldLabel, undo, carriedNote, carriedNoteReview, review, describeKey, describe, describeCarried, keyRight, keyLeft, keyTab, checked, unchecked, on, off, empty, announce*). English defaults. |
Methods
| Name | Description |
|---|---|
carry(names?) | Same as the bar button: carries the previous answers into the untouched, unchanged, non-volatile fields (or only the given names) and marks them as carried over. Emits mv-carry (source "api"). Returns the names carried. |
accept(name) | Reuses the previous answer for one field as a deliberate, already confirmed choice. Emits mv-carry (source "api"). Returns false when there is no suggestion or it was cancelled. |
confirm(name?) | Confirms one carried-over field, or all of them. Emits mv-confirm. |
undo(name?) | Puts carried-over fields (all, or one) back to what they held before the carry, with input/change events. |
checkReview() | With require-review: true when nothing carried is left unconfirmed, otherwise shows the review message, focuses the first field, emits mv-review-required and returns false. |
saveLoop() | Stores the current answers as a loop now (replacing the last one in the same period), resets touched/carried state, emits mv-loop-saved. Returns { loop, t, values }. |
clearHistory() | Forgets every stored loop (browser storage and the previous property). |
Events
| Name | Description |
|---|---|
mv-carry | Cancelable, before any reuse. detail: { fields, values, source: "key" | "chip" | "bulk" | "api", from: { t, loop } }. preventDefault() cancels; editing or deleting entries of detail.values rewrites what is written (bump a week number, drop a field). |
mv-loop-saved | A loop was stored. detail: { loop, t, values, updated (same period replaced), streak, loops, dates }. Persist it to your server here when storage is none. |
mv-confirm | Carried-over fields were confirmed. detail: { fields }. |
mv-review-required | Submit (or checkReview()) was blocked by require-review. detail: { fields }. |
Content structure
| Name | Description |
|---|---|
(children) | A <form> or any region with named controls (the component may also sit inside a form). Controls are grouped by name (or id); the chip is appended inside the closest .mv-field, fieldset or [data-same-again-field], or right after the control. |
CSS classes
| Name | Description |
|---|---|
mv-same-again-bar | Loop bar (role=group): .mv-same-again-loop (icon + “Loop 42”), -info (-meta, -streak with -dots / -dot[data-done][data-current] and -streak-text), -same (mv-button with -count) and -note (carried-over / review message with Undo; data-blocked when submit was refused). data-saved after a save. |
mv-same-again-slot | Under each field (data-state="suggest | carried", data-focus while its field is focused): .mv-same-again-chip (-icon, -when, -value, -run, -key keycap) or .mv-same-again-badge (-tag, then .mv-same-again-link buttons Confirm and Undo; data-blocked during review). |
[data-same-again-flash] | Set for ~0.7 s on a control whose value was just reused: its text settles from the ghost color, staggered across a bulk carry. |
CSS variables
| Name | Default | Description |
|---|---|---|
--mv-same-again-color | var(--mv-accent) | Tint of chips, carried-over badges and fields, streak dots and the loop pill. |
--mv-same-again-ghost | accent mixed into --mv-fg-subtle | Color of the inline ghost placeholder and of the landing animation. |
--mv-same-again-review | var(--mv-warning) | Color of the review message and of the badges of fields blocking submit. |
Accessibility
Nothing is ever filled or submitted behind the user’s back: suggestions are only offered, “Same as yesterday” acts on a click, and carried-over answers stay marked in text (“Carried over from yesterday”, not only a tint) until confirmed or edited. Each control with a suggestion gets an aria-describedby pointing to a hidden description that names the previous answer and the shortcut (“Answer from yesterday: Remote. Press Right Arrow to reuse it, Escape to dismiss.”; the button path is described for selects, radios and checkboxes), replaced by “Carried over from yesterday, not confirmed yet.” after a carry and removed once the field is settled. The shortcut only acts in an empty text field with no modifier (Left Arrow in right-to-left text), so caret movement is never hijacked, and Tab keeps navigating unless tab-accepts is set; the tip is written in the demo and in every chip’s keycap. Chips, Confirm and Undo are real <button>s whose accessible names contain their visible text plus the field (“Same as yesterday: Remote”, “Confirm Blockers”). When the pressed button disappears (a spent chip, Confirm, Undo, the bulk button), focus goes to its field, or to the first carried field, never to the page. Reuse, carry (with the list of fields), confirmation, undo, dismissal and saving are announced in a polite live region; a blocked submit is announced assertively with the fields to review and focus moves to the first one. The bar is a labelled group (“Loop 42”) and its streak dots are aria-hidden next to the streak written in words. Reused values are written through native setters and dispatch input/change, so framework state and validation stay in sync, and the author’s placeholder is restored as soon as the ghost is no longer needed. Reduced motion (OS or data-motion="reduce"): no landing, chip or note animation. Forced colors: dashed system outline on carried-over controls, bordered chips and tags, Highlight streak dots.