Exclusivobeta

Same Again <mv-same-again>

Formularios recurrentes que recuerdan la vuelta anterior: envuelve un standup diario, una hoja de horas, un relevo de turno, una checklist o un informe semanal y cada envío guarda las respuestas con su fecha (almacenamiento del navegador bajo una clave, o tu servidor mediante la propiedad previous y mv-loop-saved). La vez siguiente, cada campo ofrece su última respuesta justo donde se escribe: un placeholder fantasma teñido dentro de los campos de texto vacíos y un pequeño chip bajo cualquier campo (“Ayer · Remoto · 5 seguidas”); → en un campo vacío (Tab con tab-accepts) o un clic la reutiliza, Escape la descarta. Una barra resume la vuelta (“Vuelta 42 · Rellenado por última vez ayer a las 9:12”, una tira de puntos de 7 periodos y “Racha de 14 días”) con “Igual que ayer”, que rellena solo los campos que el usuario no ha tocado y marca cada uno como “Traído de ayer · Confirmar · Deshacer” hasta que se confirme o se edite, para que nadie envíe respuestas desfasadas sin darse cuenta; require-review bloquea el envío hasta entonces. Los campos cuya respuesta cambió en cada vuelta reciente no se sugieren, los campos bajo data-same-again="never" (y las fechas, contraseñas, datos de tarjeta, códigos de un solo uso, campos ocultos y de archivo) nunca se guardan, cadence (daily, weekdays, weekly, none) rige la racha y reemplaza un reenvío del mismo periodo en lugar de contar una vuelta nueva, y cada reutilización pasa por un mv-carry cancelable cuyos valores la app puede reescribir. Se admiten formularios nativos, grupos de radios y casillas, selects múltiples, switches y custom elements asociados a formularios, y los valores se escriben con los setters nativos y eventos input/change para que el estado del framework siga sincronizado.

CategoríaFormularios
TipoWeb Component (<mv-same-again>)
Estadobeta
KitFormularios que no pierden a nadie
También instalabutton
Keywordsexclusive, 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 observations): mark those fields data-same-again="never" or skip the component

Instalación

node scripts/add.mjs same-again --out ./src/marvelous

Agente de IA con el servidor MCP de Marvelous UI: install_components({ slugs: ["same-again"], target_dir: "<absolute path>/src/marvelous", framework: "react" }).

Archivos copiados (dependencias incluidas): tokens/tokens.css, core/base.css, core/dom.js, core/element.js, core/motion.js, components/same-again/same-again.js, components/same-again/same-again.css, components/button/button.css.

Uso

Inicio rápido, el marcado mínimo que funciona:

<mv-same-again 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>

Marcado de referencia: parte de él y personalízalo con atributos, data-* y variables 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>

Referencia cultural

Groundhog Day, Harold Ramis (1993, película). Un hombre se despierta una y otra vez en el mismo día, y en cada vuelta reutiliza todo lo que aprendió en las anteriores, así que el día repetido le sale cada vez más rápido y mejor. En la interfaz, un formulario recurrente recuerda la vuelta anterior y vuelve a ofrecer cada respuesta, campo por campo, mientras todo lo traído sigue marcado a la vista hasta que el usuario lo confirma.

API

Attributes

NameTipoDefaultDescription
keystring<pathname>:<form id | name | action>Storage key of the loop history (stored as mv-same-again:<key>). Set it per form and per person when several recurring forms share a URL.
storagelocal | session | nonelocalWhere loops are kept in the browser. none keeps nothing locally: feed the history with the previous property and persist it from mv-loop-saved.
cadencedaily | weekdays | weekly | nonedailyRhythm 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).
keepnumber10How many loops are retained (the loop counter keeps counting beyond it).
volatile-afternumber3A 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-agetime ("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-reviewbooleanSubmit 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-acceptsbooleanTab also reuses the suggestion of an empty text field (focus stays; the next Tab moves on). Off by default so Tab always navigates.
suggestinline | chipinlineinline 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-onsubmit | manualsubmitsubmit 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.
localeBCP 47 tagen-USLocale of weekdays, dates and times (“Friday”, “Sep 12”, “9:12 AM”).
data-same-again (on a field or ancestor)never | always | manualnever: 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)booleanAn 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 | volatileStyling hook written by the component on each control. data-same-again-ghost is set while the placeholder shows the previous answer.

Properties

NameTipoDescription
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.
loopsArray<{ t, values, loop? }>Stored loops, oldest first (read-only copy).
loopnumberNumber of the loop being filled now: the last loop’s number when it belongs to the current period, the next one otherwise (read-only).
streaknumberConsecutive periods with a loop, ending with the current period or the previous one (read-only).
carriedstring[]Names of the carried-over fields not confirmed yet (read-only).
stringsPartial<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

NameDescription
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

NameDescription
mv-carryCancelable, 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-savedA 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-confirmCarried-over fields were confirmed. detail: { fields }.
mv-review-requiredSubmit (or checkReview()) was blocked by require-review. detail: { fields }.

Content structure

NameDescription
(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

NameDescription
mv-same-again-barLoop 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-slotUnder 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

NameDefaultDescription
--mv-same-again-colorvar(--mv-accent)Tint of chips, carried-over badges and fields, streak dots and the loop pill.
--mv-same-again-ghostaccent mixed into --mv-fg-subtleColor of the inline ghost placeholder and of the landing animation.
--mv-same-again-reviewvar(--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”). 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.

Esta página se tradujo con IA. Informar de un problema de traducción