Exclusivobeta
Lacuna <mv-lacuna>
Exclusão gradual e no lugar para listas e grades, um substituto dos toasts de desfazer que aguenta quando você exclui várias coisas seguidas. Um item apagado não some: ele fica onde está por alguns segundos (6 por padrão, cada item com o próprio relógio) e se desfaz à vista, com as letras caindo uma a uma em uma ordem determinística, as imagens ficando cinzas e desfocadas, as bordas se esfumando e o item inteiro encolhendo um pouco, com uma pílula “Erasing… Click to keep” e uma linha fina de tempo restante. Clicar nele (ou Enter, Space, Ctrl/Cmd+Z para o mais recente) o mantém intacto com uma rematerialização, e passar o mouse pausa o relógio dele; caso contrário, ele colapsa (altura nas listas, FLIP dos blocos restantes nas grades), sai do DOM e dispara mv-erase para que o app confirme a exclusão real. Uma barra de status fixa resume tudo (“Erasing 3 items · Keep all · Erase now”), as exclusões pendentes são confirmadas ou mantidas quando se sai da página, e o texto real do DOM nunca é reescrito, então o resgate não perde nada.
| Categoria | Exibição de dados |
|---|---|
| Tipo | Web Component (<mv-lacuna>) |
| Status | beta |
| Kit | Ações destrutivas e segredos com segurança |
| Keywords | exclusive, 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
Instalação
node scripts/add.mjs lacuna --out ./src/marvelousAgente de IA com o servidor MCP do Marvelous UI: install_components({ slugs: ["lacuna"], 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/focus.js, core/motion.js, components/lacuna/lacuna.js, components/lacuna/lacuna.css.
Uso
Início rápido, a menor marcação 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>Marcação de referência, para usar como ponto de partida e personalizar com atributos, data-* e variáveis 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>Referência cultural
Brilho Eterno de uma Mente sem Lembranças, Michel Gondry (roteiro de Charlie Kaufman) (2004, filme). As memórias são apagadas uma a uma enquanto ele as revive, esmaecendo e se desfazendo diante dele, e ele tenta esconder a que quer guardar antes que ela desapareça. Aqui, os itens excluídos se dissolvem no lugar ao longo de alguns segundos e podem ser resgatados simplesmente agarrando-os antes que sumam.
API
Attributes
| Name | Tipo | Default | Description |
|---|---|---|---|
duration | time | 6s | How 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" | true | Hovering 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-leave | commit | keep | commit | What 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. |
layout | list | grid | How 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). | |
label | string | Pending erasures | Accessible name of the status bar region. |
data-lacuna-item | marker attribute | Marks the items. Without any, the direct element children of <mv-lacuna> are the items. | |
data-lacuna-erase | marker attribute | On a button (or link) inside an item: starts erasing that item. Clicking it again while the item erases keeps it. | |
data-lacuna-media | marker attribute | Inside 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-label | string | On 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-text | marker attribute | On 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-layout | set by the component | On items: erasing | erased, while the clock is held, list | grid. The host gets data-erasing while anything is pending. |
Properties
| Name | Tipo | Description |
|---|---|---|
items | Element[] | Read-only: the items currently managed (marked items, or direct children). |
pending | Element[] | Read-only: items currently erasing, oldest first. |
Methods
| Name | Description |
|---|---|
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
| Name | Description |
|---|---|
mv-erase-start | Cancelable, before an item starts erasing. detail: { item, data (the item's dataset, without data-lacuna-*), label }. preventDefault() refuses it (e.g. item locked). |
mv-rescue | An item was kept. detail: { item, data, label }. |
mv-erase | An item completed, collapsed and was removed from the DOM: commit the real delete now. detail: { item (detached, text intact), data, label }. |
Content structure
| Name | Description |
|---|---|
(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
| Name | Description |
|---|---|
mv-lacuna-ui | Overlay 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 / -real | Temporary 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-bar | Sticky status region: .mv-lacuna-bar-dot, .mv-lacuna-bar-count, .mv-lacuna-keep-all and .mv-lacuna-erase-now buttons. |
CSS variables
| Name | Tipo | Default | Description |
|---|---|---|---|
--mv-lacuna-color | var(--mv-accent) | Progress line, paused pill and status dot. | |
--mv-lacuna-feather | 12px | How far the edges of the content dissolve at the end. | |
--mv-lacuna-pill-inset | var(--mv-space-3) | Distance of the pill from the inline end of a list row. | |
--_p | output | Set 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.