独家

Lacuna <mv-lacuna>

列表和网格的原地渐进式删除,可替代撤销提示,连续删除多项时依然可靠。

文化出处

暖暖内含光,米歇尔·冈瑞(编剧:查理·考夫曼)(2004 年,电影)。记忆在他重温的同时被一段段抹去,在他眼前褪色、瓦解,他试图在那段想要保留的记忆消失之前把它藏起来。在这里,被删除的项目会在原地用几秒钟逐渐消散,只要在它们消失前抓住,就能把它们救回来。

工作原理

就地渐进式删除,适用于列表和网格,可替代撤销提示框,并且在连续删除多项时依然好用。被擦除的项目不会立即消失:它会在原位停留几秒(默认 6 秒,每个项目各自计时),并肉眼可见地逐渐瓦解:字母按确定的顺序一个个脱落,图片变灰变模糊,边缘羽化,整个项目略微缩小,同时显示 “Erasing… Click to keep” 胶囊标签和一条细细的剩余时间线。点击它(或按 Enter、Space,按 Ctrl/Cmd+Z 恢复最近一项)即可通过重新成形的动画将其完整保留,悬停会暂停它的计时;否则它会收起(列表中收起高度,网格中对其余卡片执行 FLIP 动画),离开 DOM 并触发 mv-erase,由应用执行真正的删除。固定的状态栏会汇总情况(“Erasing 3 items · Keep all · Erase now”),离开页面时,待擦除的项目会被提交或保留,而且真实的 DOM 文本从不会被改写,因此恢复是无损的。

分类数据展示
类型Web Component(<mv-lacuna>)
状态稳定版
套件安全处理破坏性操作和机密信息
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天 needs quick, reversible removal without a confirmation dialog for eac小时 ite分
  • 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

安装

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

使用 Marvelous UI MCP 服务器的 AI 智能体:install_components({ slugs: ["lacuna"], target_dir: "<absolute path>/src/marvelous", framework: "react" })。

复制的文件(含依赖):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。

用法

快速开始,最小可运行标记:

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

标准标记,可在此基础上通过属性、data-* 和 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>

API

Attributes

Name类型DefaultDescription
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

Name类型Description
itemsElement[]Read-only: the items currently managed (marked items, or direct children).
pendingElement[]Read-only: items currently erasing, oldest first.
stringsPartial<Record<string, string>>Overrides for every visible and announced text (keys: keepAll, eraseNow, hint, pillClick, pillTap, pausedClick, pausedTap, barCount ({count}), itemOne / itemOther ({n}), item, announceErasing ({label}), announceErasingAway ({label}, {undo}), undo, undoMac, kept / erased ({label}), keptMany / erasedMany ({count})). English defaults; can be set before the element is defined.

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

Name类型DefaultDescription
--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”, “Command Z” on Apple devices, 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”). Every text, visible or announced, can be translated through the strings property. 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.

本页面由 AI 翻译。报告翻译问题