Exclusifbêta

Same Again <mv-same-again>

Des formulaires récurrents qui se souviennent de la boucle précédente : englobez un stand-up quotidien, une feuille de temps, une passation d’équipe, une checklist ou un rapport hebdomadaire, et chaque envoi enregistre les réponses avec leur date (stockage du navigateur sous une clé, ou votre serveur via la propriété previous et mv-loop-saved). La fois suivante, chaque champ propose sa dernière réponse à l’endroit même de la saisie : un placeholder fantôme teinté dans les champs texte vides et une petite pastille sous n’importe quel champ (« Hier · Télétravail · 5 d’affilée ») ; → dans un champ vide (Tab avec tab-accepts) ou un clic la réutilise, Échap la rejette. Une barre résume la boucle (« Boucle 42 · Dernier remplissage hier à 9 h 12 », une frise de points sur 7 périodes et « Série de 14 jours ») avec « Comme hier », qui ne remplit que les champs que l’utilisateur n’a pas touchés et marque chacun « Reporté d’hier · Confirmer · Annuler » jusqu’à ce qu’il soit confirmé ou modifié, pour que personne n’envoie de réponses périmées sans s’en apercevoir ; require-review bloque l’envoi d’ici là. Les champs dont la réponse a changé à chaque boucle récente ne sont pas suggérés, les champs sous data-same-again="never" (ainsi que les dates, mots de passe, données de carte, codes à usage unique, champs masqués et champs de fichier) ne sont jamais enregistrés, cadence (daily, weekdays, weekly, none) pilote la série et remplace un nouvel envoi sur la même période au lieu de compter une nouvelle boucle, et chaque réutilisation passe par un mv-carry annulable dont l’application peut réécrire les valeurs. Les formulaires natifs, les groupes de boutons radio et de cases à cocher, les selects multiples, les interrupteurs et les custom elements associés aux formulaires sont pris en charge, et les valeurs sont écrites via les setters natifs avec des événements input/change, pour que l’état du framework reste synchronisé.

CatégorieFormulaires
TypeWeb Component (<mv-same-again>)
Statutbêta
KitDes formulaires qui ne perdent personne
Installe aussibutton
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

Installation

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

Agent IA avec le serveur MCP Marvelous UI : install_components({ slugs: ["same-again"], target_dir: "<absolute path>/src/marvelous", framework: "react" }).

Fichiers copiés (dépendances comprises) : 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.

Utilisation

Démarrage rapide, le balisage minimal qui fonctionne :

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

Balisage de référence : partez de celui-ci et personnalisez-le avec les attributs, data-* et les 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>

Référence culturelle

Un jour sans fin, Harold Ramis (1993, film). Un homme se réveille encore et encore le même jour, et à chaque boucle il réutilise tout ce qu’il a appris lors des précédentes : la journée répétée devient plus rapide et meilleure. Dans l’interface, un formulaire récurrent se souvient de la boucle précédente et repropose chaque réponse, champ par champ, tandis que tout ce qui est reporté reste visiblement marqué jusqu’à ce que l’utilisateur le confirme.

API

Attributes

NameTypeDefaultDescription
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

NameTypeDescription
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.

Cette page a été traduite par IA. Signaler un problème de traduction