익스클루시브

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로 번역되었습니다. 번역 문제 신고