Exclusivobeta

Lacuna <mv-lacuna>

Eliminación gradual y en su sitio para listas y cuadrículas, que sustituye a los toasts de deshacer y aguanta cuando borras varias cosas seguidas. Un elemento borrado no desaparece: se queda donde está unos segundos (6 por defecto, cada elemento con su propio reloj) y se deshace a la vista, con las letras cayéndose una a una en un orden determinista, las imágenes volviéndose grises y borrosas, los bordes difuminándose y todo el elemento encogiéndose un poco, con una píldora “Erasing… Click to keep” y una fina línea de tiempo restante. Al hacer clic en él (o con Enter, Space, o Ctrl/Cmd+Z para el último) se conserva intacto con una rematerialización, y al pasar el puntero se detiene su reloj; si no, se colapsa (altura en las listas, FLIP de las fichas restantes en las cuadrículas), sale del DOM y lanza mv-erase para que la app confirme el borrado real. Una barra de estado fija lo resume (“Erasing 3 items · Keep all · Erase now”), los borrados pendientes se confirman o se conservan al salir de la página, y el texto real del DOM nunca se reescribe, así que el rescate no pierde nada.

CategoríaVisualización de datos
TipoWeb Component (<mv-lacuna>)
Estadobeta
KitAcciones destructivas y secretos seguros
Keywordsexclusive, culture, delete, undo, soft-delete, deferred-delete, rescue, undo-toast-alternative, list, grid, files, notes, photos, fade, dissolve, text-effect, flip, keyboard

When to use

  • Users delete several notes, files or messages in a row and must be able to change their mind about any one of them
  • A deletion should stay visible where it happened instead of moving to a toast in a corner of the screen
  • A photo or file gri d needs quick, reversible removal without a confirmation dialog for eac h ite min
  • The server delete should only be committed once the user has had a real chance to take it back

Avoid when

  • The deletion is destructive and irreversible on a whole project or account, and needs explicit consent → use Hold Button instead
  • A single deletion must be confirmed with context before anything happens → use Dialog instead
  • The action happens outside a visible list (a detail page that navigates away), so there is nothing to watch fade → use Toast instead

Instalación

node scripts/add.mjs lacuna --out ./src/marvelous

Agente de IA con el servidor MCP de Marvelous UI: install_components({ slugs: ["lacuna"], 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/focus.js, core/motion.js, components/lacuna/lacuna.js, components/lacuna/lacuna.css.

Uso

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

<mv-lacuna>
  <p><strong>Q3 plan.pdf</strong> <button data-lacuna-erase>Delete</button></p>
  <p><strong>Old logo.png</strong> <button data-lacuna-erase>Delete</button></p>
</mv-lacuna>

Marcado de referencia: parte de él y personalízalo con atributos, data-* y variables CSS:

<div id="lc-demo" style="width:min(100%,46rem);margin-inline:auto">
  <style>
    #lc-demo { display:grid; gap:1.75rem }
    #lc-demo .lc-card { border:1px solid var(--mv-border); border-radius:var(--mv-radius-xl); background:var(--mv-surface); box-shadow:var(--mv-shadow-xs) }
    #lc-demo .lc-head { display:flex; align-items:center; justify-content:space-between; gap:.75rem 1rem; flex-wrap:wrap; padding:1rem 1.25rem; border-bottom:1px solid var(--mv-border) }
    #lc-demo .lc-head h3 { margin:0; font-size:.9375rem; letter-spacing:-.01em }
    #lc-demo .lc-head p { margin:.125rem 0 0; color:var(--mv-fg-muted); font-size:.8125rem }
    #lc-demo .lc-actions { display:flex; gap:.5rem; flex-wrap:wrap }
    #lc-demo mv-lacuna { padding-bottom:.75rem }
    #lc-demo .lc-list { list-style:none; margin:0; padding:.375rem .5rem 0 }
    #lc-demo .lc-row { display:flex; align-items:center; gap:.875rem; padding:.625rem .75rem; border-radius:var(--mv-radius-lg); outline:none; transition:background-color var(--mv-duration-fast) var(--mv-ease-out) }
    #lc-demo .lc-row:hover { background:var(--mv-bg-subtle) }
    #lc-demo .lc-row:focus-visible { box-shadow:var(--mv-focus-ring) }
    #lc-demo .lc-icon { display:grid; place-items:center; flex:none; width:2.25rem; height:2.25rem; border-radius:var(--mv-radius-md); background:color-mix(in oklch, var(--lc-tone) 14%, var(--mv-surface)); color:var(--lc-tone) }
    #lc-demo .lc-icon svg { width:1.05rem; height:1.05rem }
    #lc-demo .lc-body { flex:1; min-width:0 }
    #lc-demo .lc-body strong { display:block; font-size:.875rem; font-weight:600; white-space:nowrap; overflow:hidden; text-overflow:ellipsis }
    #lc-demo .lc-body > span { display:block; margin-top:.125rem; color:var(--mv-fg-muted); font-size:.75rem }
    #lc-demo .lc-del { flex:none; opacity:.55; transition:opacity var(--mv-duration-fast) var(--mv-ease-out) }
    #lc-demo .lc-row:hover .lc-del, #lc-demo .lc-row:focus-within .lc-del { opacity:1 }
    #lc-demo .lc-foot { display:flex; align-items:center; justify-content:space-between; gap:.75rem; flex-wrap:wrap; padding:.75rem 1.25rem; border-top:1px solid var(--mv-border); color:var(--mv-fg-muted); font-size:.8125rem }
    #lc-demo .lc-foot kbd { font:500 .6875rem/1 var(--mv-font-mono); padding:.15rem .35rem; border:1px solid var(--mv-border-strong); border-bottom-width:2px; border-radius:var(--mv-radius-xs); color:var(--mv-fg) }
    #lc-demo .lc-grid { display:grid; grid-template-columns:repeat(auto-fill,minmax(10rem,1fr)); gap:.75rem; padding:1rem 1.25rem 0 }
    #lc-demo .lc-photo { position:relative; margin:0; border-radius:var(--mv-radius-lg); outline:none }
    #lc-demo .lc-photo:focus-visible { box-shadow:var(--mv-focus-ring) }
    #lc-demo .lc-img { aspect-ratio:4/3; border-radius:var(--mv-radius-lg); box-shadow:inset 0 0 0 1px oklch(0 0 none / .06) }
    #lc-demo .lc-photo figcaption { padding:.5rem .125rem .5rem }
    #lc-demo .lc-photo strong { display:block; font-size:.8125rem; font-weight:600 }
    #lc-demo .lc-photo figcaption > span { display:block; color:var(--mv-fg-muted); font-size:.75rem }
    #lc-demo .lc-photo .lc-x { position:absolute; top:.4rem; right:.4rem; display:grid; place-items:center; width:1.75rem; height:1.75rem; border:0; border-radius:var(--mv-radius-full); background:oklch(0.15 0 none / .55); color:white; cursor:pointer; opacity:0; transition:opacity var(--mv-duration-fast) var(--mv-ease-out); backdrop-filter:blur(6px) }
    #lc-demo .lc-photo:hover .lc-x, #lc-demo .lc-photo:focus-within .lc-x { opacity:1 }
    #lc-demo .lc-photo .lc-x:focus-visible { opacity:1; outline:none; box-shadow:var(--mv-focus-ring) }
    #lc-demo .lc-photo .lc-x svg { width:.9rem; height:.9rem }
    @media (hover:none) { #lc-demo .lc-photo .lc-x { opacity:1 } }
  </style>

  <!-- 1 · Files list -->
  <section class="lc-card" aria-labelledby="lc-files-title">
    <div class="lc-head">
      <div>
        <h3 id="lc-files-title">Recent files</h3>
        <p>Delete a few in a row, then click one before it fades to keep it.</p>
      </div>
      <div class="lc-actions">
        <button class="mv-button" data-variant="outline" data-size="sm" type="button" id="lc-three">Erase three at once</button>
        <button class="mv-button" data-variant="ghost" data-size="sm" type="button" id="lc-restore">Restore demo</button>
      </div>
    </div>
    <mv-lacuna id="lc-files" duration="6s" label="Files being erased">
      <ul class="lc-list" id="lc-files-list">
        <li class="lc-row" data-lacuna-item data-id="f-104" tabindex="0" style="--lc-tone:var(--mv-info)">
          <span class="lc-icon" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><path d="M14 2v6h6M9 13h6M9 17h4"/></svg></span>
          <span class="lc-body"><strong>Q3 roadmap draft</strong><span>Doc · Edited 2 hours ago by Priya Raman</span></span>
          <button class="mv-button lc-del" data-variant="ghost" data-size="icon" type="button" data-lacuna-erase aria-label="Delete Q3 roadmap draft"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 7h16M10 11v6M14 11v6M5 7l1 12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2l1-12M9 7V4h6v3"/></svg></button>
        </li>
        <li class="lc-row" data-lacuna-item data-id="f-099" tabindex="0" style="--lc-tone:var(--mv-success)">
          <span class="lc-icon" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M3 9h18M3 15h18M9 3v18"/></svg></span>
          <span class="lc-body"><strong>Budget 2027 - v2.xlsx</strong><span>Spreadsheet · 184 KB · Shared with Finance</span></span>
          <button class="mv-button lc-del" data-variant="ghost" data-size="icon" type="button" data-lacuna-erase aria-label="Delete Budget 2027 - v2.xlsx"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 7h16M10 11v6M14 11v6M5 7l1 12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2l1-12M9 7V4h6v3"/></svg></button>
        </li>
        <li class="lc-row" data-lacuna-item data-id="f-097" tabindex="0" style="--lc-tone:var(--mv-accent)">
          <span class="lc-icon" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20h9M16.5 3.5a2.1 2.1 0 0 1 3 3L7 19l-4 1 1-4Z"/></svg></span>
          <span class="lc-body"><strong>Interview notes: Maya Patel</strong><span>Note · Yesterday at 4:12 PM</span></span>
          <button class="mv-button lc-del" data-variant="ghost" data-size="icon" type="button" data-lacuna-erase aria-label="Delete Interview notes: Maya Patel"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 7h16M10 11v6M14 11v6M5 7l1 12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2l1-12M9 7V4h6v3"/></svg></button>
        </li>
        <li class="lc-row" data-lacuna-item data-id="f-093" tabindex="0" style="--lc-tone:var(--mv-warning)">
          <span class="lc-icon" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="9" cy="9" r="2"/><path d="m21 15-5-5L5 21"/></svg></span>
          <span class="lc-body"><strong>Brand refresh moodboard</strong><span>Board · 36 images · Edited Monday</span></span>
          <button class="mv-button lc-del" data-variant="ghost" data-size="icon" type="button" data-lacuna-erase aria-label="Delete Brand refresh moodboard"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 7h16M10 11v6M14 11v6M5 7l1 12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2l1-12M9 7V4h6v3"/></svg></button>
        </li>
        <li class="lc-row" data-lacuna-item data-id="f-090" tabindex="0" style="--lc-tone:var(--mv-info)">
          <span class="lc-icon" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="18" height="18" rx="2"/><path d="M16 2v4M8 2v4M3 10h18"/></svg></span>
          <span class="lc-body"><strong>Team offsite agenda: Lisbon</strong><span>Doc · Oct 14-16 · 3 comments</span></span>
          <button class="mv-button lc-del" data-variant="ghost" data-size="icon" type="button" data-lacuna-erase aria-label="Delete Team offsite agenda: Lisbon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 7h16M10 11v6M14 11v6M5 7l1 12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2l1-12M9 7V4h6v3"/></svg></button>
        </li>
        <li class="lc-row" data-lacuna-item data-id="f-086" tabindex="0" style="--lc-tone:var(--mv-success)">
          <span class="lc-icon" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 3v18h18"/><path d="m7 15 4-4 3 3 5-6"/></svg></span>
          <span class="lc-body"><strong>Pricing experiment results</strong><span>Report · $12.4k uplift · Sep 2</span></span>
          <button class="mv-button lc-del" data-variant="ghost" data-size="icon" type="button" data-lacuna-erase aria-label="Delete Pricing experiment results"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 7h16M10 11v6M14 11v6M5 7l1 12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2l1-12M9 7V4h6v3"/></svg></button>
        </li>
        <li class="lc-row" data-lacuna-item data-id="f-081" tabindex="0" style="--lc-tone:var(--mv-accent)">
          <span class="lc-icon" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 7h-9M14 17H5"/><circle cx="17" cy="17" r="3"/><circle cx="7" cy="7" r="3"/></svg></span>
          <span class="lc-body"><strong>Release notes 4.2</strong><span>Draft · 1,240 words · Not published</span></span>
          <button class="mv-button lc-del" data-variant="ghost" data-size="icon" type="button" data-lacuna-erase aria-label="Delete Release notes 4.2"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 7h16M10 11v6M14 11v6M5 7l1 12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2l1-12M9 7V4h6v3"/></svg></button>
        </li>
        <li class="lc-row" data-lacuna-item data-id="f-077" tabindex="0" style="--lc-tone:var(--mv-fg-muted)">
          <span class="lc-icon" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 8v13H3V8M1 3h22v5H1zM10 12h4"/></svg></span>
          <span class="lc-body"><strong>Old logo exports.zip</strong><span>Archive · 48 MB · Uploaded in March</span></span>
          <button class="mv-button lc-del" data-variant="ghost" data-size="icon" type="button" data-lacuna-erase aria-label="Delete Old logo exports.zip"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 7h16M10 11v6M14 11v6M5 7l1 12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2l1-12M9 7V4h6v3"/></svg></button>
        </li>
      </ul>
    </mv-lacuna>
    <div class="lc-foot">
      <span id="lc-log" aria-live="off">Focus a row and press <kbd>Delete</kbd>; <kbd>Ctrl</kbd> <kbd>Z</kbd> keeps the latest.</span>
      <span id="lc-count">8 files</span>
    </div>
  </section>

  <!-- 2 · Photo grid -->
  <section class="lc-card" aria-labelledby="lc-photos-title">
    <div class="lc-head">
      <div>
        <h3 id="lc-photos-title">Trip to the coast · 8 photos</h3>
        <p>Hovering a fading photo holds its clock.</p>
      </div>
    </div>
    <mv-lacuna id="lc-photos" duration="5s" layout="grid" label="Photos being erased">
      <div class="lc-grid" id="lc-photos-grid">
        <figure class="lc-photo" data-lacuna-item data-id="p-01" tabindex="0"><div class="lc-img" data-lacuna-media style="background:linear-gradient(180deg,oklch(0.82 0.08 230) 0%,oklch(0.9 0.05 80) 52%,oklch(0.55 0.1 220) 53%,oklch(0.42 0.09 235) 100%)"></div><figcaption><strong>Harbor at noon</strong><span>Sep 12 · 4.1 MB</span></figcaption><button class="lc-x" type="button" data-lacuna-erase aria-label="Delete Harbor at noon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" aria-hidden="true"><path d="M18 6 6 18M6 6l12 12"/></svg></button></figure>
        <figure class="lc-photo" data-lacuna-item data-id="p-02" tabindex="0"><div class="lc-img" data-lacuna-media style="background:radial-gradient(circle at 70% 38%,oklch(0.93 0.1 85) 0 7%,transparent 8%),linear-gradient(180deg,oklch(0.62 0.16 30) 0%,oklch(0.78 0.14 60) 48%,oklch(0.36 0.08 280) 49%,oklch(0.26 0.06 270) 100%)"></div><figcaption><strong>Sunset from the pier</strong><span>Sep 12 · 3.8 MB</span></figcaption><button class="lc-x" type="button" data-lacuna-erase aria-label="Delete Sunset from the pier"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" aria-hidden="true"><path d="M18 6 6 18M6 6l12 12"/></svg></button></figure>
        <figure class="lc-photo" data-lacuna-item data-id="p-03" tabindex="0"><div class="lc-img" data-lacuna-media style="background:linear-gradient(160deg,transparent 55%,oklch(0.48 0.09 150) 56%),linear-gradient(200deg,transparent 48%,oklch(0.58 0.1 140) 49%),linear-gradient(180deg,oklch(0.88 0.04 220),oklch(0.8 0.05 200))"></div><figcaption><strong>Cliff trail</strong><span>Sep 13 · 5.2 MB</span></figcaption><button class="lc-x" type="button" data-lacuna-erase aria-label="Delete Cliff trail"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" aria-hidden="true"><path d="M18 6 6 18M6 6l12 12"/></svg></button></figure>
        <figure class="lc-photo" data-lacuna-item data-id="p-04" tabindex="0"><div class="lc-img" data-lacuna-media style="background:radial-gradient(ellipse at 50% 120%,oklch(0.85 0.06 85) 0 38%,transparent 39%),linear-gradient(180deg,oklch(0.7 0.1 230),oklch(0.52 0.11 225))"></div><figcaption><strong>Dunes, low tide</strong><span>Sep 13 · 2.9 MB</span></figcaption><button class="lc-x" type="button" data-lacuna-erase aria-label="Delete Dunes, low tide"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" aria-hidden="true"><path d="M18 6 6 18M6 6l12 12"/></svg></button></figure>
        <figure class="lc-photo" data-lacuna-item data-id="p-05" tabindex="0"><div class="lc-img" data-lacuna-media style="background:repeating-linear-gradient(90deg,oklch(0.72 0.13 25) 0 14%,oklch(0.86 0.08 90) 14% 28%,oklch(0.62 0.1 200) 28% 42%,oklch(0.9 0.03 80) 42% 56%)"></div><figcaption><strong>Beach huts</strong><span>Sep 14 · 4.6 MB</span></figcaption><button class="lc-x" type="button" data-lacuna-erase aria-label="Delete Beach huts"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" aria-hidden="true"><path d="M18 6 6 18M6 6l12 12"/></svg></button></figure>
        <figure class="lc-photo" data-lacuna-item data-id="p-06" tabindex="0"><div class="lc-img" data-lacuna-media style="background:radial-gradient(circle at 30% 30%,oklch(0.96 0.02 90) 0 10%,transparent 40%),linear-gradient(180deg,oklch(0.8 0.02 240),oklch(0.62 0.03 230))"></div><figcaption><strong>Morning fog</strong><span>Sep 14 · 3.1 MB</span></figcaption><button class="lc-x" type="button" data-lacuna-erase aria-label="Delete Morning fog"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" aria-hidden="true"><path d="M18 6 6 18M6 6l12 12"/></svg></button></figure>
        <figure class="lc-photo" data-lacuna-item data-id="p-07" tabindex="0"><div class="lc-img" data-lacuna-media style="background:radial-gradient(circle at 50% 55%,oklch(0.78 0.14 60) 0 16%,oklch(0.6 0.12 45) 17% 22%,transparent 23%),linear-gradient(135deg,oklch(0.42 0.05 60),oklch(0.3 0.04 50))"></div><figcaption><strong>Café on the corner</strong><span>Sep 15 · 2.4 MB</span></figcaption><button class="lc-x" type="button" data-lacuna-erase aria-label="Delete Café on the corner"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" aria-hidden="true"><path d="M18 6 6 18M6 6l12 12"/></svg></button></figure>
        <figure class="lc-photo" data-lacuna-item data-id="p-08" tabindex="0"><div class="lc-img" data-lacuna-media style="background:linear-gradient(180deg,oklch(0.24 0.05 270) 0%,oklch(0.36 0.08 290) 60%,oklch(0.55 0.12 330) 100%),radial-gradient(circle,white 1px,transparent 1px)"></div><figcaption><strong>Night drive home</strong><span>Sep 15 · 3.3 MB</span></figcaption><button class="lc-x" type="button" data-lacuna-erase aria-label="Delete Night drive home"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" aria-hidden="true"><path d="M18 6 6 18M6 6l12 12"/></svg></button></figure>
      </div>
    </mv-lacuna>
  </section>

  <script type="module">
    await customElements.whenDefined("mv-lacuna");
    const files = document.getElementById("lc-files");
    const photos = document.getElementById("lc-photos");
    const log = document.getElementById("lc-log");
    const count = document.getElementById("lc-count");
    const templates = new Map([
      [files, document.getElementById("lc-files-list").cloneNode(true)],
      [photos, document.getElementById("lc-photos-grid").cloneNode(true)],
    ]);
    const syncCount = () => { const n = files.items.length; count.textContent = `${n} file${n === 1 ? "" : "s"}`; };
    for (const el of [files, photos]) {
      el.addEventListener("mv-erase", (e) => { log.textContent = `Deleted “${e.detail.label}” (${e.detail.data.id}), committed to the server.`; syncCount(); });
      el.addEventListener("mv-rescue", (e) => { log.textContent = `Kept “${e.detail.label}”.`; });
    }
    document.getElementById("lc-three").addEventListener("click", () => {
      const list = files.items.filter((item) => !files.pending.includes(item));
      [1, 3, 4].map((i) => list[i]).filter(Boolean).forEach((item, k) => setTimeout(() => files.erase(item), k * 220));
    });
    document.getElementById("lc-restore").addEventListener("click", () => {
      for (const [el, tpl] of templates) {
        el.rescueAll();
        const current = el.querySelector(".lc-list, .lc-grid");
        current.replaceWith(tpl.cloneNode(true));
      }
      syncCount();
      log.textContent = "Demo restored: 8 files and 8 photos.";
    });
  </script>
</div>

Referencia cultural

Eternal Sunshine of the Spotless Mind, Michel Gondry (guion de Charlie Kaufman) (2004, película). Sus recuerdos se borran uno a uno mientras los revive, desvaneciéndose y deshaciéndose delante de él, y él intenta esconder el que quiere conservar antes de que desaparezca. Aquí los elementos eliminados se disuelven en su sitio durante unos segundos y pueden rescatarse simplemente atrapándolos antes de que desaparezcan.

API

Attributes

NameTipoDefaultDescription
durationtime6sHow long an erased item stays and comes apart before it is committed: 6s, 4500ms, or a bare number of milliseconds. Changing it applies to pending items too.
pause-on-hover"true" | "false"trueHovering an erasing item holds its clock (the pill turns to “Paused · Click to keep”). The hover that was already there when the erase started (the pointer on the Delete button) does not count: the pointer has to leave and come back.
on-leavecommit | keepcommitWhat happens to pending erasures on pagehide (navigation, reload, tab close, back-forward cache) or when the component is removed from the DOM: commit finishes them synchronously and emits mv-erase for each (send them with sendBeacon/keepalive); keep restores them silently.
layoutlist | gridHow an erased item leaves: list collapses its height, grid fades it and slides the remaining tiles into the gap (FLIP), with a centered pill. Auto-detected when omitted (another item on the same row means grid).
labelstringPending erasuresAccessible name of the status bar region.
data-lacuna-itemmarker attributeMarks the items. Without any, the direct element children of <mv-lacuna> are the items.
data-lacuna-erasemarker attributeOn a button (or link) inside an item: starts erasing that item. Clicking it again while the item erases keeps it.
data-lacuna-mediamarker attributeInside an item: an element treated as an image (greys and blurs faster), for CSS backgrounds or custom media. <img>, <video>, <picture> and <canvas> are detected on their own.
data-lacuna-labelstringOn an item: name used in announcements and events. Otherwise the first heading, <strong>, <figcaption> or [data-lacuna-title], else the item text (60 characters max).
data-lacuna-keep-textmarker attributeOn an element inside an item: its text is left alone (no letter drop), e.g. for text a framework re-renders during the erasure.
data-lacuna-state / data-lacuna-paused / data-lacuna-layoutset by the componentOn items: erasing | erased, while the clock is held, list | grid. The host gets data-erasing while anything is pending.

Properties

NameTipoDescription
itemsElement[]Read-only: the items currently managed (marked items, or direct children).
pendingElement[]Read-only: items currently erasing, oldest first.

Methods

NameDescription
erase(item | index)Starts erasing an item. Returns false if it is already erasing or if mv-erase-start was cancelled.
rescue(item | index)Keeps an erasing item: its text and attributes are restored as they were, with a short re-materialization. Returns false if it was not erasing.
rescueAll()Keeps every pending item (“Keep all”). Returns how many.
commitAll()Finishes every pending erasure immediately (“Erase now”). Returns how many.

Events

NameDescription
mv-erase-startCancelable, before an item starts erasing. detail: { item, data (the item's dataset, without data-lacuna-*), label }. preventDefault() refuses it (e.g. item locked).
mv-rescueAn item was kept. detail: { item, data, label }.
mv-eraseAn item completed, collapsed and was removed from the DOM: commit the real delete now. detail: { item (detached, text intact), data, label }.

Content structure

NameDescription
(content)A list or grid: the items themselves as direct children, or any structure (ul, div grid…) with [data-lacuna-item] elements. The status bar and a visually hidden live region are appended after it.

CSS classes

NameDescription
mv-lacuna-uiOverlay added to an erasing item: .mv-lacuna-pill (icon + .mv-lacuna-pill-text) and .mv-lacuna-line (.mv-lacuna-line-fill, scaled to the remaining time).
mv-lacuna-text / -char / -realTemporary wrapper of each text node: the aria-hidden twin made of .mv-lacuna-char letters ([data-gone] once dropped) and the visually hidden real node.
mv-lacuna-barSticky status region: .mv-lacuna-bar-dot, .mv-lacuna-bar-count, .mv-lacuna-keep-all and .mv-lacuna-erase-now buttons.

CSS variables

NameTipoDefaultDescription
--mv-lacuna-colorvar(--mv-accent)Progress line, paused pill and status dot.
--mv-lacuna-feather12pxHow far the edges of the content dissolve at the end.
--mv-lacuna-pill-insetvar(--mv-space-3)Distance of the pill from the inline end of a list row.
--_poutputSet on an erasing item: progress 0-1, usable for your own degradation styles.

Accessibility

Items keep their own role and semantics; while erasing, an item gets aria-describedby pointing to “Erasing. Press Enter to keep it.” and becomes focusable (tabindex=-1) if it was not, and focus stays where it was (usually its Delete button), so Enter or Space on it, or anywhere inside it, keeps it; Delete or Backspace on a focused item (outside text fields) starts erasing it; Ctrl/Cmd+Z keeps the most recent erasure page-wide, never while typing in a field. A polite role=status announces “Erasing ‘Q3 roadmap draft’. Press Enter to keep it.” (or “Press Control Z” when focus is elsewhere), “Kept ‘…’.”, “Erased ‘…’.” and the bulk results (“Kept 3 items.”). The screen-reader text is the real, untouched text node (moved into a visually hidden span) while the dropping letters are an aria-hidden twin, so assistive tech keeps reading the true content until the end. When an item that holds focus leaves, focus moves to the next item (its Delete button when that is where it was), else the previous one, else the list. The status bar is a labelled region with two real buttons (“Keep all”, “Erase now”) and is never the only way to act. A double click on Delete does not undo itself. Touch: tap to keep (the pill says “Tap to keep”). Reduced motion (OS setting or data-motion="reduce"): no letter drop, blur, shrink, feathering or FLIP, only a fade and the remaining-time line, and the item disappears without a collapse; the clock itself still runs.

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