익스클루시브

Self-Destruct <mv-self-destruct>

한 번 표시된 뒤 실제로 파기되는 일회성 비밀 값 표시(API 키, 복구 코드, 공유 비밀번호, 임시 링크)입니다.

문화적 레퍼런스

미션 임파서블, 브루스 겔러 (1966, 시리즈). 각 지령은 읽는 데 필요한 시간 동안만 존재하고 그 뒤에는 자동으로 파기됩니다. UI에서는 그 메커니즘만 가져왔습니다. 일회성 비밀 정보는 요청할 때 공개되고, 단순한 카운트다운 막대가 비워지는 동안 읽을 수 있으며, 그 후 DOM과 메모리에서 실제로 제거됩니다.

작동 방식

한 번 보여 준 뒤 완전히 파기하는 일회성 비밀 정보 표시(API 키, 복구 코드, 공유 비밀번호, 임시 링크)입니다. 처음에는 중립적인 덮개 뒤에 봉인되어 있어 DOM에 비밀 정보가 전혀 없습니다. “Reveal”을 누르면 위쪽 가장자리를 따라 비워지는 얇은 막대와 카운트다운 링(기본 30초)으로 표시되는 카운트다운이 시작됩니다. 내장 Copy 버튼은 남은 시간을 줄일 수 있고(after-copy), 선택적으로 끝날 때 클립보드를 비울 수 있습니다. 탭이 숨겨지면 비밀 정보를 파기하거나, 버튼을 누르고 있는 동안에만 읽을 수 있게 할 수도 있습니다. 0이 되면 문자가 뒤섞이며 떨어져 나가고, 비밀 정보의 모든 텍스트 노드가 제거되며 JS 참조도 해제됩니다. 그 자리에는 묘비(“This secret was destroyed · 2:41 PM”)가 표시되고, “Generate a new key” 동작을 위한 슬롯이 제공됩니다.

카테고리데이터 표시
유형Web Component (<mv-self-destruct>)
상태안정
키트안전한 파괴적 작업과 비밀 정보
Keywordsexclusive, culture, secret, one-time, api-key, token, recovery-codes, password, reveal, countdown, timer, copy, clipboard, security, privacy, ephemeral, hold-to-view, disintegrate

When to use

  • A freshly generated API key, token or webhook secret must be shown exactly once after creation
  • Two-factor recovery codes are displayed for the user to save before they leave the page
  • A shared password or temporary link should be readable briefly, then gone from the screen
  • Support or admin tools reveal a sensitive value on demand and must not leave it lying on screen

Avoid when

  • The value is not sensitive and should stay visible to be copied at any time → use Copy Button instead
  • A destructive action needs a deliberate confirmation rather than a timed reveal → use Hold Button instead
  • The secret must stay out of the page source: never server-render it inside the template, set the secret property from JS

설치

node scripts/add.mjs self-destruct --out ./src/marvelous

Marvelous UI MCP 서버를 사용하는 AI 에이전트: install_components({ slugs: ["self-destruct"], target_dir: "<absolute path>/src/marvelous", framework: "react" }).

복사되는 파일(의존성 포함): tokens/tokens.css, core/base.css, core/dom.js, core/element.js, core/motion.js, components/self-destruct/self-destruct.js, components/self-destruct/self-destruct.css.

사용법

빠른 시작, 동작하는 가장 작은 마크업:

<mv-self-destruct label="API key" seconds="30">
  <template>mvk_live_4f9a2c7e81d0</template>
</mv-self-destruct>

기본 마크업입니다. 여기서 시작해 속성, data-*, CSS 변수로 커스터마이즈하세요:

<div id="sd-demo" style="width:min(100%,70rem);margin-inline:auto">
  <style>
    #sd-demo { display:grid; gap:1.25rem }
    #sd-demo .sd-grid { display:grid; grid-template-columns:repeat(auto-fit, minmax(19rem, 1fr)); gap:1.25rem; align-items:start }
    #sd-demo .sd-card { display:grid; gap:.875rem; padding:1.125rem 1.125rem 1rem; border:1px solid var(--mv-border); border-radius:var(--mv-radius-xl); background:var(--mv-surface); box-shadow:var(--mv-shadow-xs) }
    #sd-demo .sd-head { display:grid; gap:.25rem }
    #sd-demo .sd-head h3 { display:flex; align-items:center; gap:.5rem; margin:0; font-size:.9375rem; letter-spacing:-.01em }
    #sd-demo .sd-head p { margin:0; color:var(--mv-fg-muted); font-size:.8125rem; line-height:1.45 }
    #sd-demo .sd-log { margin:0; min-height:1.25rem; color:var(--mv-fg-subtle); font:400 .75rem/1.5 var(--mv-font-mono); overflow:hidden; text-overflow:ellipsis; white-space:nowrap }
    #sd-demo .sd-log b { color:var(--mv-fg-muted); font-weight:600 }
    #sd-demo .sd-top { display:flex; align-items:center; justify-content:space-between; gap:1rem; flex-wrap:wrap }
    #sd-demo .sd-top p { margin:0; color:var(--mv-fg-muted); font-size:.8125rem }
    #sd-demo #sd-codes { --mv-self-destruct-font-size:.9375rem }
  </style>

  <div class="sd-top">
    <p>Each secret is shown once. Reveal, copy, or wait for the countdown to run out.</p>
    <button class="mv-button" data-variant="outline" data-size="sm" type="button" id="sd-reset">Generate again</button>
  </div>

  <div class="sd-grid">
    <!-- 1 · Fresh API key: 30s, shortened to 5s after a copy, clipboard wiped at the end -->
    <section class="sd-card" aria-labelledby="sd-key-title">
      <div class="sd-head">
        <h3 id="sd-key-title">Production API key <span class="mv-badge" data-variant="success" data-size="sm">New</span></h3>
        <p>Store it in your secrets manager now. For your security, we can’t show it again.</p>
      </div>
      <mv-self-destruct id="sd-key" label="API key" seconds="30" after-copy="5" clear-clipboard>
        <button slot="destroyed" class="mv-button" data-size="sm" type="button" data-regen="sd-key">Generate a new key</button>
      </mv-self-destruct>
      <p class="sd-log" id="sd-key-log" aria-live="off">30s · 5s after copy · clipboard wiped</p>
    </section>

    <!-- 2 · Recovery codes from a <template>: grouped by 4, destroyed if the tab is hidden -->
    <section class="sd-card" aria-labelledby="sd-codes-title">
      <div class="sd-head">
        <h3 id="sd-codes-title">Two-factor recovery codes</h3>
        <p>Each code works once. Print them or save them offline before they disappear.</p>
      </div>
      <mv-self-destruct id="sd-codes" label="Recovery codes" seconds="60" group="4" on-hide>
        <template>
          <li>7k2m9x4q</li><li>p8w3n6fa</li><li>c5j1t9vr</li><li>h4d7s2ly</li>
          <li>q9b6e3mz</li><li>u2g8k5wn</li><li>r7x4c1pt</li><li>e3v9h6jd</li>
        </template>
        <button slot="destroyed" class="mv-button" data-variant="outline" data-size="sm" type="button" data-regen="sd-codes">Regenerate codes</button>
      </mv-self-destruct>
      <p class="sd-log" id="sd-codes-log" aria-live="off">60s · groups of 4 · ends on tab switch</p>
    </section>

    <!-- 3 · Shared password: visible only while held -->
    <section class="sd-card" aria-labelledby="sd-pass-title">
      <div class="sd-head">
        <h3 id="sd-pass-title">Shared vault password</h3>
        <p>Maya Chen shared “Office Wi-Fi · Guest” with you. It stays readable only while you hold.</p>
      </div>
      <mv-self-destruct id="sd-pass" label="Password" seconds="20" hold>
        <button slot="destroyed" class="mv-button" data-variant="outline" data-size="sm" type="button" data-regen="sd-pass">Request again</button>
      </mv-self-destruct>
      <p class="sd-log" id="sd-pass-log" aria-live="off">20s · press and hold (or hold Space) to view</p>
    </section>
  </div>

  <script type="module">
    await customElements.whenDefined("mv-self-destruct");
    const root = document.getElementById("sd-demo");
    const pick = (alphabet, n) => {
      const bytes = crypto.getRandomValues(new Uint8Array(n));
      return Array.from(bytes, (b) => alphabet[b % alphabet.length]).join("");
    };
    const lower = "abcdefghjkmnpqrstuvwxyz23456789";
    const make = {
      "sd-key": () => `mvk_live_${pick("ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz0123456789", 32)}`,
      "sd-codes": () => Array.from({ length: 8 }, () => pick(lower, 8)),
      "sd-pass": () => `${pick("ABCDEFGHJKLMNPQRSTUVWXYZ", 1)}${pick(lower, 5)}-${pick(lower, 4)}-${pick("0123456789", 2)}${pick("!#%&*?", 1)}`,
    };
    const initial = {
      "sd-key": "30s · 5s after copy · clipboard wiped",
      "sd-codes": "60s · groups of 4 · ends on tab switch",
      "sd-pass": "20s · press and hold (or hold Space) to view",
    };
    const time = () => new Date().toLocaleTimeString("en-US", { hour: "numeric", minute: "2-digit", second: "2-digit" });
    const log = (id, text) => {
      const el = document.getElementById(`${id}-log`);
      el.replaceChildren();
      const b = document.createElement("b");
      b.textContent = text;
      el.append(b, ` · ${time()}`);
    };

    document.getElementById("sd-key").secret = make["sd-key"]();
    document.getElementById("sd-pass").secret = make["sd-pass"]();

    for (const id of Object.keys(make)) {
      const el = document.getElementById(id);
      el.addEventListener("mv-reveal", (e) => log(id, `mv-reveal { seconds: ${e.detail.seconds} }`));
      el.addEventListener("mv-copy", (e) => log(id, `mv-copy { remaining: ${e.detail.remaining.toFixed(1)} }`));
      el.addEventListener("mv-destroy", (e) => log(id, `mv-destroy { reason: "${e.detail.reason}" }`));
    }
    const regen = (id) => {
      document.getElementById(id).secret = make[id]();
      document.getElementById(`${id}-log`).textContent = initial[id];
    };
    root.addEventListener("click", (e) => {
      const btn = e.target.closest("[data-regen]");
      if (btn) regen(btn.dataset.regen);
    });
    document.getElementById("sd-reset").addEventListener("click", () => Object.keys(make).forEach(regen));
  </script>
</div>

API

Attributes

Name유형DefaultDescription
secondsnumber30How long the secret stays available once revealed (1-86400). The countdown is wall-clock based: it keeps running in a background tab and when the element is moved in the DOM.
after-copynumberAfter a successful copy (Copy button, or selecting the value and pressing Ctrl/Cmd+C), shortens the remaining time to this many seconds if more is left. The destruction reason then becomes "copy". Absent: copying does not change the countdown.
clear-clipboardbooleanAt destruction, overwrites the clipboard with an empty string if the last copy made in this document was the secret. Best effort: it only works while the page has focus (browsers reject clipboard writes from a background tab), and it cannot know whether something else was copied in another app since; in that case it would overwrite it.
on-hidebooleanDestroys the revealed secret as soon as the tab becomes hidden (tab switch, minimized window, locked screen). Reason "hidden".
holdbooleanHold-to-view mode: the value is rendered only while the “Hold to view” button is pressed (pointer, touch, or Space / Enter held) and removed from the DOM again on release. The first press starts the countdown; Copy stays available while the countdown runs.
groupnumberVisually groups characters by n ("4" → 7k2m 9x4q) using margins only, so copies and selections stay exact. Applies to each item of a list.
no-copybooleanHides the built-in Copy button (the value can still be selected by hand).
labelstringSecretWhat the secret is (“API key”): accessible name of the panel and wording of the announcements.
reveal-labelstringRevealLabel of the reveal button (hold mode: “Hold to view” / “Release to hide”).
destroyed-labelstringThis secret was destroyedTombstone title, followed by the time of destruction (en-US, e.g. 2:41 PM).
data-stateempty | sealed | revealed | destroying | destroyedSet by the component.
data-urgencysoon | finalSet by the component in the last 10 / 5 seconds (countdown bar and ring turn warning, then danger).
data-peekbooleanSet by the component while the value is held open (hold mode).

Properties

Name유형Description
secretstring | string[]Write-only. Loads a new secret and seals it (cancelling any countdown, replacing a tombstone): a string, a multi-line string (one item per line) or an array (rendered as a numbered list). Reading it always returns null so the value can't be scraped back from the element. Preferred over the <template> child. Can be set before the element is defined.
hasSecretbooleanRead-only: a secret is loaded and not yet destroyed.
statestringRead-only: empty | sealed | revealed | destroying | destroyed.
remainingnumberRead-only: seconds left (sealed: the full duration; destroyed: 0).

Methods

NameDescription
reveal()Shows the secret and starts the countdown (in hold mode: on the first press). Returns false unless sealed.
copy()Copies the secret (Clipboard API, hidden-textarea fallback) and applies after-copy. Resolves to true on success.
destroy(reason?)Destroys now (default reason "manual"); also works while sealed. The secret leaves the DOM synchronously, before the disintegration plays.

Events

NameDescription
mv-revealThe secret was revealed (hold mode: first press). detail: { seconds }.
mv-copyThe secret was copied. detail: { remaining }, never the secret itself.
mv-destroyThe secret was purged (fired right away; the animation then plays for about a second). detail: { reason: "timeout" | "manual" | "hidden" | "copy" | "removed", at: Date }. "removed": the element was detached from the page while revealed.

Content structure

NameDescription
<template> childAlternative to the secret property: its (inert) content is read once at mount, then the template is removed. Text lines, or one element per item (<li>, <code>…), or a single list element.
destroyedContent shown in the tombstone once destroyed, e.g. a “Generate a new key” button. Receives focus if focus was inside the component.

CSS classes

NameDescription
mv-self-destruct-panelLive card (role=group, named by label): countdown bar, stage, action bar.
mv-self-destruct-stage / -cover / -mask / -noteValue area and the neutral cover shown while sealed (fixed placeholder, never derived from the secret).
mv-self-destruct-value / -code / -chunk / -listRendered secret (monospace), its groups and the numbered list for several items.
mv-self-destruct-countdown / -countdown-fillTop-edge countdown bar: a faint track and a fill that empties toward the start edge.
mv-self-destruct-timer / -ring / -secsCountdown ring and “Self-destructs in 24s”.
mv-self-destruct-btnReveal / hold, Copy (data-state copied | error) and Destroy now buttons.
mv-self-destruct-tombstone / -slotWhat remains after destruction: title, time, hint and the destroyed slot.

CSS variables

NameDefaultDescription
--mv-self-destruct-colorvar(--mv-accent)Countdown bar and ring color while time is comfortable.
--mv-self-destruct-soonvar(--mv-warning)Color in the last 10 seconds.
--mv-self-destruct-finalvar(--mv-danger)Color in the last 5 seconds and during destruction.
--mv-self-destruct-font-sizevar(--mv-text-base)Size of the monospace value.
--mv-self-destruct-radiusvar(--mv-radius-lg)Corner radius of the panel and tombstone.

Accessibility

The panel is a role=group named by label (“API key”). Sealed, it contains only a fixed placeholder and a text note (“Hidden · visible for 30s once revealed”); the Reveal button is a real <button>. After reveal, focus moves to the value (tabindex=-1, focus ring) so screen readers read it, then Tab reaches Destroy now and Copy. A polite role=status region announces the reveal (“API key revealed. It will be destroyed in 30 seconds.”), a 10-second warning, copies (“Copied to clipboard. Destroying in 5 seconds.”) and the destruction; the ticking seconds are visible text but not a live region, so nothing chatters every second. Hold mode: the button works with pointer, touch (no context menu, no scroll while holding) and Space / Enter held (key repeat ignored, blur releases); the value region is aria-live=polite so it is read out when it appears. At destruction the characters are replaced by random glyphs synchronously and the value is aria-hidden during the animation; the tombstone is focusable (tabindex=-1) and focus waits on the panel during the animation (the buttons are disabled), then moves to the destroyed slot's first control (or the tombstone) if it was inside the component; regenerating from there lands on the new Reveal button. Urgency is never color alone: the seconds count and the ring length carry it. Reduced motion (system or data-motion="reduce"): no scramble or fall: the value is masked and fades out, then the tombstone fades in; the countdown bar and ring still move because they carry the remaining time. Security notes: nothing secret is in the DOM before reveal nor after destruction, event details never contain the secret, the secret getter returns null; JavaScript strings cannot be zeroed, so “removed from memory” means every reference is dropped for the garbage collector. Content in a <template> is present in the HTML source: prefer the secret property. Known limit: apart from label, reveal-label and destroyed-label, the built-in texts (Copy, countdown notes, announcements) are English only and cannot be overridden yet.

이 페이지는 AI로 번역되었습니다. 번역 문제 신고