Exclusivobeta

Saved State <mv-saved-state>

Linha de status de salvamento automático (Unsaved changes · Saving… · Saved · 2 minutes ago · Offline · Couldn’t save · Retry) controlada por saving() / saved() / failed() ou track(promise), ou apontando-a para um formulário. O que as versões feitas à mão erram: ela nunca pisca (“Saving…” só para salvamentos mais lentos que 400 ms, “Saved” mantido por pelo menos 1 s), nunca diz “Saved” quando está offline ou quando edições digitadas durante o salvamento ainda estão pendentes, e só anuncia os erros, a perda de conexão e a volta dela.

CategoriaFeedback
TipoWeb Component (<mv-saved-state>)
Statusbeta
Keywordsexclusive, light, autosave, saving, saved, status, offline, unsaved, beforeunload, retry

When to use

  • A document, note or settings page saves on its own and should say whether the latest edits are safe
  • A form saves in the background and users need a truthful Offline or Couldn’t save message with a way to retry
  • Leaving the page with edits still pending should ask for confirmation

Avoid when

  • Changes are queued locally and synced later, with a pending list, conflicts and background retries → use Bench Sync instead
  • The form saves only when the user clicks Save and pending edits should grow more insistent → use Tell-Tale instead
  • The result of a one-off action should appear briefly and then disappear → use Toast instead

Instalação

node scripts/add.mjs saved-state --out ./src/marvelous

Agente de IA com o servidor MCP do Marvelous UI: install_components({ slugs: ["saved-state"], target_dir: "<absolute path>/src/marvelous", framework: "react" }).

Arquivos copiados (dependências incluídas): tokens/tokens.css, core/base.css, core/dom.js, core/element.js, core/motion.js, components/saved-state/saved-state.js, components/saved-state/saved-state.css.

Uso

Início rápido, a menor marcação que funciona:

<mv-saved-state for="doc-form" guard></mv-saved-state>
<!-- in your autosave code: savedState.track(fetch("/api/doc", { method: "PUT", body })) -->

Marcação de referência, para usar como ponto de partida e personalizar com atributos, data-* e variáveis CSS:

<div style="display:grid;gap:1.25rem;width:100%;max-width:560px">
  <form id="mv-saved-demo-doc" class="mv-card" style="padding:0;overflow:hidden">
    <div style="display:flex;align-items:center;justify-content:space-between;gap:1rem;padding:.7rem 1rem;border-bottom:1px solid var(--mv-border)">
      <span style="display:flex;align-items:center;gap:.5rem;min-width:0;font-size:.8rem;color:var(--mv-fg-muted)">
        <span>Marketing</span><span aria-hidden="true">/</span><span style="color:var(--mv-fg);font-weight:600">Q4 launch brief</span>
      </span>
      <mv-saved-state id="mv-saved-demo" for="mv-saved-demo-doc" guard></mv-saved-state>
    </div>
    <div style="display:grid;gap:.6rem;padding:1rem">
      <input class="mv-input" name="title" value="Aurora 2.0 launch, Oct 14" aria-label="Title">
      <textarea class="mv-textarea" name="body" rows="4" aria-label="Brief">Launch in the US, Brazil and Japan on Tuesday, October 14. Early-bird pricing at $49/year until October 31. Owners: Priya Raman (press), Kenji Watanabe (store listings), Lucía Ortega (support macros).</textarea>
    </div>
  </form>

  <div style="display:flex;flex-wrap:wrap;gap:.5rem 1.5rem;font-size:.85rem">
    <label class="mv-choice"><input type="checkbox" role="switch" class="mv-switch" id="mv-saved-demo-slow"><span class="mv-choice-text">Slow network</span></label>
    <label class="mv-choice"><input type="checkbox" role="switch" class="mv-switch" id="mv-saved-demo-fail"><span class="mv-choice-text">Server error</span></label>
    <label class="mv-choice"><input type="checkbox" role="switch" class="mv-switch" id="mv-saved-demo-offline"><span class="mv-choice-text">Offline</span></label>
  </div>
  <p style="margin:-.5rem 0 0;font-size:.8rem;color:var(--mv-fg-subtle)">Type to autosave. Saves faster than 400 ms never flash “Saving…”.</p>

  <div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(15rem,1fr));gap:.6rem 1.5rem;padding-top:1rem;border-top:1px solid var(--mv-border)">
    <mv-saved-state id="mv-saved-demo-a"></mv-saved-state>
    <mv-saved-state id="mv-saved-demo-b"></mv-saved-state>
    <mv-saved-state id="mv-saved-demo-c"></mv-saved-state>
    <mv-saved-state id="mv-saved-demo-d"></mv-saved-state>
  </div>
</div>

<script type="module">
  const form = document.getElementById("mv-saved-demo-doc");
  const status = document.getElementById("mv-saved-demo");
  const $ = (id) => document.getElementById(`mv-saved-demo-${id}`);
  const ago = (min) => new Date(Date.now() - min * 60e3).toISOString();
  status.setAttribute("saved-at", ago(2));

  // Stand-in for fetch(): fast by default, slow, failing or offline on demand.
  const save = () => new Promise((resolve, reject) => setTimeout(() => {
    if ($("offline").checked) reject(new TypeError("Failed to fetch"));
    else if ($("fail").checked) reject(new Error("The server answered 503 Service Unavailable"));
    else resolve();
  }, $("slow").checked ? 1800 : 250));

  let timer = 0;
  form.addEventListener("submit", (e) => e.preventDefault());
  form.addEventListener("input", () => {
    clearTimeout(timer);
    timer = setTimeout(() => status.track(save()).catch(() => {}), 800);
  });
  status.addEventListener("mv-retry", (e) => e.detail.waitUntil(save()));
  $("offline").addEventListener("change", (e) => dispatchEvent(new Event(e.target.checked ? "offline" : "online")));

  // One of each state.
  $("a").setAttribute("saved-at", ago(180));
  customElements.whenDefined("mv-saved-state").then(() => {
    $("b").unsaved();
    $("c").saving();
    $("d").failed(new Error("Your session expired"));
    $("d").addEventListener("mv-retry", (e) => e.detail.waitUntil(new Promise((r) => setTimeout(r, 1200))));
  });
</script>

API

Attributes

NameTipoDefaultDescription
forform idForm whose input and change events mark the line “Unsaved changes”. Without it, the closest ancestor form, if any.
guardbooleanAsks for confirmation before leaving the page while edits are unsaved, saving or failed. The beforeunload listener exists only during those states.
saved-atISO dateTime of the last save known when the page loads (server-rendered). Shows “Saved · …” from the start.
localeBCP 47en-USLocale for the relative time (Intl.RelativeTimeFormat) and the full date in the tooltip.
data-stateidle | unsaved | saving | saved | offline | errorSet by the component: the state currently displayed (for styling).

Properties

NameTipoDescription
stateidle | unsaved | saving | saved | errorRead-only. The state reported by the app, before the anti-flicker timing and the offline overlay.
lastSavedDate | nullRead-only. Time of the last successful save.
errorunknownRead-only. The value passed to failed(), until the next successful save.
onlinebooleanRead-only. Connectivity as seen through navigator.onLine and the online / offline events.
stringsPartial<Record<string, string>>Overrides for every visible or announced text: idle (""), unsaved, saving, saved ("Saved · {time}"), justNow, savedTitle ("Last saved {date}"), offline, offlineIdle, backOnline, error, retry. English defaults.

Methods

NameDescription
unsaved()Edits are pending. During a save, remembers them so the next saved() shows “Unsaved changes” instead of “Saved”. Ignored while an error is shown.
saving()A save started. Displayed only if it lasts longer than 400 ms.
saved(at?)The save succeeded (at: Date or ISO string, default now). “Saved · just now”, then the time updates quietly.
failed(error?)The save failed. Shows “Couldn’t save · Retry”; error.message becomes the tooltip.
track(promise)saving(), then saved() or failed(reason) when the promise settles. Only the latest tracked promise counts. Returns the promise.
reset()Back to idle, forgetting the last save and error.

Events

NameDescription
mv-retryRetry was clicked, or the connection came back after a failure. detail = { reason: "click" | "online", error, waitUntil(promise) }. Pass your save to waitUntil (synchronously) to track it. Cancelable: preventDefault() keeps the error on screen. If nobody handles it, the line falls back to “Unsaved changes”.

CSS classes

NameDescription
mv-saved-state-icon / -text / -retryGenerated parts: the icon (aria-hidden), the status text (holds a <time> element when saved) and the Retry button.

CSS variables

NameDefaultDescription
--mv-saved-state-colorvar(--mv-fg-muted)Text and neutral icon color.
--mv-saved-state-successvar(--mv-success)Check icon when saved.
--mv-saved-state-unsavedvar(--mv-warning)Dot icon for unsaved changes.
--mv-saved-state-offlinevar(--mv-warning)Offline icon.
--mv-saved-state-errorvar(--mv-danger)Text and icon color on failure.
--mv-saved-state-font-sizevar(--mv-text-sm)Text size.
--mv-saved-state-icon-size1.05emIcon size.

Accessibility

Plain text in the page, so it can be read at any time without being chatty: the visible line is not a live region, and the relative time updates silently. A separate polite role=status region announces only the transitions that matter: “Couldn’t save”, going offline (“Offline · will save when you’re back”) and “Back online”; routine Unsaved / Saving / Saved changes are never announced, and the message is cleared after a few seconds so it is not read twice later. The state is carried by the words, never by the icon alone (icons are aria-hidden and use currentColor). Retry is a real <button> with a visible focus ring; if it disappears while focused, focus moves to the status text instead of being lost. The saved time is a <time datetime> with the full date as a tooltip. Reduced motion (OS or data-motion="reduce"): the spinner stops and state changes swap instantly. Forced colors: system text and link colors. The beforeunload guard is attached only while edits are pending, keeping the back-forward cache usable otherwise.

Esta página foi traduzida com IA. Informar um problema de tradução