익스클루시브
Deep Pause <mv-deep-pause>
단순한 켜기/끄기 스위치 대신 대상 전체를 일관되게 일시 중지합니다(온라인 스토어의 휴가 모드, 구독, 워크스페이스, 연동, 광고 캠페인, 휴가를 떠나는 사람).
문화적 레퍼런스
잠자는 숲속의 미녀(La Belle au bois dormant), 샤를 페로(1697년, 동화). 공주가 백 년의 잠에 빠지자 착한 요정은 성안의 모든 사람을 함께 재워, 아무것도 그사이에 흘러가지 않은 채 모든 것이 정확히 그대로 같은 순간에 깨어나게 합니다. UI에서는 상점, 구독 또는 연동 전체를 일시 정지하면 그에 속한 모든 것이 함께 멈추고, 무엇이 잠들고 무엇이 깨어 있는지 나열하며, 그사이에 도착한 것을 집계하고, 깨어날 때 모든 것을 한꺼번에 다시 시작하면서 정확히 그대로 깨어나지 못한 몇 가지를 짚어 줍니다.
작동 방식
단순한 켜기/끄기 스위치가 아니라, 하나의 대상 전체(온라인 상점의 휴가 모드, 구독, 워크스페이스, 연동, 광고 캠페인, 휴가를 떠나는 사람)를 위한 일관된 일시 정지입니다. 일시 정지하기 전에, 목록 두 개로 함께 멈추는 것과 계속 실행되는 것을 밝힌 인벤토리를 보여 줍니다. 여기에는 앱이 허용하는 항목별 예외(“Keep taking bookings”), 날짜로 계산된 인라인 주의 사항(“The FALL20 code ends on October 6, before it wakes”), 재개 계획(수동, 또는 “in 24 days”와 함께 글로 풀어 쓴 날짜), 그사이에 도착하는 것에 대한 정책(나중을 위해 보류, 재개 날짜를 반영하는 편집 가능한 답장으로 거절, 또는 전달)이 포함됩니다. mv-pause는 취소할 수 있고, 그 waitUntil(promise)은 백엔드가 실제로 모든 것을 멈추는 동안 UI를 대기 상태로 유지하므로, 일시 정지 상태는 실제로 그렇게 된 뒤에만 표시됩니다. 일시 정지 중에는 차분하게 계속 표시되는 카드가 잠든 시간, 재개 계획, 수신 정책, 보류된 항목의 실시간 카운터(앱에서 호출하는 hold(id, n): “1,284 webhook events queued”), 정확히 그대로 재개되지 않을 수 있는 항목, 접을 수 있는 인벤토리를 보여 주며, 재개 날짜는 그 자리에서 바꿀 수 있습니다. 재개(수동, 재개 시각에 예약 실행, 또는 코드에서 호출)는 취소 가능한 mv-wake를 거치며, 그 promise는 원래대로 돌아오지 못한 항목을 보고할 수 있습니다. 그사이에 날짜가 지난 항목은 자동으로 표시됩니다. 이어서 재개 요약이 재개된 것과 확인이 필요한 것을 나열하고, 보류된 각 그룹에는 mv-held-action을 통해 고유한 선택지(“Send now / Review / Discard”, 파괴적인 선택은 두 번 눌러 확인)가 주어집니다.
| 카테고리 | 피드백 |
|---|---|
| 유형 | Web Component (<mv-deep-pause>) |
| 상태 | 안정 |
| 키트 | 정직한 시스템 상태 |
| 함께 설치되는 항목 | button, input, checkbox, radio, switch, textarea |
| Keywords | exclusive, culture, pause, resume, vacation-mode, hold, snooze, suspend, freeze, subscription, workspace, integration, campaign, leave, queue, held-items, wake-date, inventory, settings |
When to use
- A store, workspace or subscription offers a vacation or pause mode that stops several things at once and must restart them together
- Pausing an integration, automation or ad campaign must say exactly what stops, what keeps running and what happens to incoming events
- An account or seat is put on hold (leave, seasonal business) and people need a wake date and a clear picture of what was held meanwhile
- On resume, queued orders, held emails or skipped jobs need an explicit decision instead of being silently sent or dropped
Avoid when
- A single setting turns on or off with no side effects to inventory → use Switch instead
- The change is a permanent removal (delete, archive, revoke) and the point is to show what depends on the item → use Wobble instead
- Access is granted for a limited time and must end on its own, like an admin mode or a trial → use Midnight instead
설치
node scripts/add.mjs deep-pause --out ./src/marvelousMarvelous UI MCP 서버를 사용하는 AI 에이전트: install_components({ slugs: ["deep-pause"], target_dir: "<absolute path>/src/marvelous", framework: "react" }).
복사되는 파일(의존성 포함): tokens/tokens.css, core/base.css, components/button/button.css, components/input/input.css, components/checkbox/checkbox.css, components/radio/radio.css, components/switch/switch.css, core/dom.js, core/element.js, components/textarea/textarea.css, components/textarea/char-count.js, core/motion.js, components/deep-pause/deep-pause.js, components/deep-pause/deep-pause.css.
사용법
빠른 시작, 동작하는 가장 작은 마크업:
<mv-deep-pause subject="CRM sync" incoming-label="webhook events">
<ul data-group="freeze"><li>Outgoing webhooks</li><li>Nightly contact import</li></ul>
<ul data-group="keep"><li>Contacts and history</li></ul>
</mv-deep-pause>기본 마크업입니다. 여기서 시작해 속성, data-*, CSS 변수로 커스터마이즈하세요:
<div id="dp-demo" style="width:min(100%,60rem);margin-inline:auto">
<style>
#dp-demo { display:grid; gap:1.75rem; align-content:start }
#dp-demo .dp-case { display:grid; gap:.75rem }
#dp-demo .dp-cap { display:flex; align-items:baseline; justify-content:space-between; gap:.5rem 1rem; flex-wrap:wrap }
#dp-demo .dp-cap h4 { margin:0; font-size:.8125rem; font-weight:600; color:var(--mv-fg-muted); letter-spacing:.04em; text-transform:uppercase }
#dp-demo .dp-cap p { margin:0; color:var(--mv-fg-subtle); font-size:.75rem }
#dp-demo .dp-bar { display:flex; align-items:center; gap:.5rem .75rem; flex-wrap:wrap; padding:.625rem .875rem; border:1px dashed var(--mv-border); border-radius:var(--mv-radius-lg); background:var(--mv-bg-subtle) }
#dp-demo .dp-bar .mv-choice { font-size:.8125rem; margin-inline-start:auto }
#dp-demo .dp-log { flex:1 1 16rem; min-width:0; margin:0; color:var(--mv-fg-subtle); font:.75rem/1.4 var(--mv-font-mono); overflow:hidden; text-overflow:ellipsis; white-space:nowrap }
</style>
<!-- 1 · Vacation mode for an online store: starts on the plan, before pausing -->
<section class="dp-case" aria-label="Store vacation mode">
<div class="dp-cap">
<h4>Store vacation mode</h4>
<p>Flip an exception, pick a wake date, then pause.</p>
</div>
<mv-deep-pause id="dp-store" subject="Harbor & Pine Ceramics" open incoming="queue" incoming-label="orders and messages" max-days="90">
<ul data-group="freeze">
<li data-id="checkout" data-detail="The storefront stays visible with a “back soon” notice">Checkout and new orders</li>
<li data-id="emails" data-detail="4 scheduled · next one goes out Friday">Marketing emails</li>
<li data-id="automations" data-detail="Abandoned cart and restock alerts">2 automations</li>
<li data-id="billing" data-detail="Your $39.00 plan pauses; the next charge moves by the pause length">Billing</li>
<li data-id="promo" data-expires-label="The FALL20 code">Fall promotion (FALL20)</li>
<li data-id="workshops" data-optional data-exception="Keep taking bookings" data-detail="Pottery workshops on Saturdays">Public booking page</li>
<li data-id="chat" data-optional data-kept data-exception="Keep answering" data-detail="Replies from your phone">Customer chat</li>
</ul>
<ul data-group="keep">
<li>Products, orders and customer data</li>
<li>API read access for your accounting app</li>
<li>Team logins (3 members)</li>
</ul>
<ul data-group="held">
<li data-id="orders" data-one="order queued" data-actions="release:Process now=Processed, review:Review~">orders queued</li>
<li data-id="mails" data-one="email held" data-actions="send:Send now=Sent, review:Review~, discard:Discard!=Discarded">emails held</li>
<li data-id="messages" data-one="message answered" data-actions="reply:Reply personally=Replied, archive:Archive=Archived">messages answered</li>
</ul>
</mv-deep-pause>
<div class="dp-bar">
<button type="button" class="mv-button" data-variant="outline" data-size="sm" id="dp-order">Simulate an order</button>
<button type="button" class="mv-button" data-variant="outline" data-size="sm" id="dp-message">Simulate a message</button>
<p class="dp-log" id="dp-store-log" aria-live="polite">Plan open: nothing is paused yet.</p>
<label class="mv-choice"><input type="checkbox" role="switch" class="mv-switch" data-size="sm" id="dp-fail"> Make the server fail</label>
</div>
</section>
<!-- 2 · An integration already paused for a few days, woken manually -->
<section class="dp-case" aria-label="Paused integration">
<div class="dp-cap">
<h4>Integration on hold</h4>
<p>Paused 3 days ago. Wake it to see what was held and what didn’t resume exactly.</p>
</div>
<mv-deep-pause id="dp-sync" subject="CRM sync · Tarnwick Traders" incoming="queue" incoming-options="queue decline" incoming-label="webhook events"></mv-deep-pause>
<div class="dp-bar">
<button type="button" class="mv-button" data-variant="outline" data-size="sm" id="dp-webhooks">Simulate 40 webhook events</button>
<p class="dp-log" id="dp-sync-log" aria-live="polite">Paused. Events are being queued.</p>
<button type="button" class="mv-button" data-variant="ghost" data-size="sm" id="dp-reset">Reset demo</button>
</div>
</section>
<script type="module">
const DAY = 864e5;
const store = document.getElementById("dp-store");
const sync = document.getElementById("dp-sync");
const fail = document.getElementById("dp-fail");
const at = (days) => new Date(Date.now() + days * DAY);
const iso = (d) => `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
const logger = (id) => (text) => {
const t = new Date().toLocaleTimeString("en-US", { hour: "numeric", minute: "2-digit", second: "2-digit" });
document.getElementById(id).textContent = `${t} · ${text}`;
};
// A fake API: 700 ms round trip, optionally failing.
const server = () => new Promise((resolve, reject) => setTimeout(() => (fail.checked ? reject(new Error("503")) : resolve()), 700));
/* 1 · Store: dates relative to today so the demo never goes stale. */
const storeLog = logger("dp-store-log");
store.wakeAt = iso(at(24));
const promo = store.items.map((i) => (i.id === "promo" ? { ...i, expires: iso(at(12)), detail: "20% off pottery, sitewide" } : i));
store.items = promo;
store.addEventListener("mv-pause", (e) => {
const { freezes, keeps, wake, incoming } = e.detail;
storeLog(`Freezing ${freezes.length} together, ${keeps.length} keep running · wake ${wake ? wake.toLocaleDateString("en-US") : "manually"} · ${incoming.policy}`);
e.detail.waitUntil(server());
});
store.addEventListener("mv-pause-error", () => storeLog("Pause failed: nothing was frozen."));
store.addEventListener("mv-wake", (e) => {
storeLog(`Waking after ${Math.round(e.detail.asleep / 1000)} s (${e.detail.reason})…`);
e.detail.waitUntil(server());
});
store.addEventListener("mv-wake-error", () => storeLog("Wake failed: still paused."));
store.addEventListener("mv-held-action", (e) => {
storeLog(`${e.detail.action} → ${e.detail.count} ${e.detail.label}`);
e.detail.waitUntil(server());
});
store.addEventListener("mv-dismiss", (e) => storeLog(e.detail.unresolved.length ? `Closed with ${e.detail.unresolved.length} held group(s) left for later.` : "All settled."));
// What arrives while the store is paused: the app counts it, the component shows it.
document.getElementById("dp-order").addEventListener("click", () => {
if (store.hold("orders") === false) storeLog("The store is open: orders go straight through.");
else storeLog("New order queued.");
});
document.getElementById("dp-message").addEventListener("click", () => {
if (store.hold("messages") === false) storeLog("The store is open: messages go straight to you.");
else storeLog("New message answered with your auto-reply.");
});
// The scheduled marketing emails get held as soon as the store is paused.
store.addEventListener("mv-pause", () => setTimeout(() => { store.hold("mails", 3); store.hold("orders", 2); }, 900));
/* 2 · Integration: inventory and held counts from JS, restored in the paused state. */
const syncLog = logger("dp-sync-log");
const setupSync = () => {
sync.items = [
{ id: "contacts", label: "Contact sync", detail: "Every 15 minutes, both directions", group: "freeze" },
{ id: "deals", label: "Deal stage webhooks", detail: "Outgoing, 3 endpoints", group: "freeze" },
{ id: "enrich", label: "Nightly enrichment job", detail: "Runs at 2:00 AM", group: "freeze" },
{ id: "token", label: "Sandbox API token", detail: "Used by the staging mirror", group: "freeze", expires: iso(at(-1)), expiresLabel: "The sandbox token" },
{ id: "read", label: "Read-only API access", group: "keep" },
{ id: "audit", label: "Audit log", group: "keep" },
{ id: "mapping", label: "Field mapping settings", group: "keep" },
];
sync.held = [
{ id: "events", label: "webhook events queued", one: "webhook event queued", count: 1284, actions: [{ id: "replay", label: "Replay in order", done: "Replayed" }, { id: "review", label: "Inspect", resolves: false }, { id: "drop", label: "Drop", tone: "danger", done: "Dropped" }] },
{ id: "jobs", label: "sync jobs skipped", one: "sync job skipped", count: 6, actions: [{ id: "run", label: "Run a catch-up sync", done: "Caught up" }, { id: "skip", label: "Skip them", done: "Skipped" }] },
];
sync.pausedAt = new Date(Date.now() - 3 * DAY - 4 * 36e5);
};
setupSync();
sync.addEventListener("mv-wake", (e) => {
syncLog(`Resuming ${e.detail.freezes.length} jobs together…`);
// The server can report what did not come back exactly.
e.detail.waitUntil(new Promise((r) => setTimeout(() => r({ changed: [{ id: "enrich", note: "Last night’s run was skipped; the next one is tonight at 2:00 AM." }] }), 700)));
});
sync.addEventListener("mv-held-action", (e) => {
syncLog(`${e.detail.action} → ${e.detail.count} ${e.detail.label}`);
e.detail.waitUntil(new Promise((r) => setTimeout(r, 600)));
});
sync.addEventListener("mv-dismiss", () => syncLog("Back to normal."));
document.getElementById("dp-webhooks").addEventListener("click", () => {
syncLog(sync.hold("events", 40) === false ? "Not paused: events are delivered live." : "40 events queued.");
});
document.getElementById("dp-reset").addEventListener("click", () => { setupSync(); syncLog("Paused. Events are being queued."); });
</script>
</div>API
Attributes
| Name | 유형 | Default | Description |
|---|---|---|---|
subject | string | Name of the thing being paused (“Harbor & Pine Ceramics”, “CRM sync”), shown as the card title and used in announcements. | |
open | boolean | Shows the pause plan (inventory, wake plan, incoming policy) while awake. Set by the “Pause…” button; removed on Cancel, Escape or once paused. | |
paused-at | ISO date-time | Moment the pause started. Set it from your server to render the paused state directly (no event); removing it returns to awake. Reflected by the component after a successful pause. | |
wake | "YYYY-MM-DD" | ISO date-time | "manual" | Planned wake moment. A date alone means the start of that day, local time. Absent or "manual" = only a person or your code wakes it. Seeds the plan while awake, drives the scheduled wake while paused, reflected after a pause or a wake-date change. | |
incoming | queue | decline | forward | queue | Default policy for what arrives while paused: hold it for the wake summary, decline it with a reply message, or forward it to someone. |
incoming-options | space-separated list | queue decline forward | Policies the app supports; the others are not offered. |
incoming-label | string | requests | What arrives, in the plural (“orders and messages”, “webhook events”), used in “While paused, new …”. |
max-days | number | Latest allowed wake date, in days from today (date input max and validation). | |
locale | BCP 47 tag | en-US | Locale of dates, relative days, durations and counts. |
heading-level | 2..6 | 3 | Level of the title heading; section and list headings follow one and two levels below (read once, at first connection). |
data-state | awake | paused | woke | Set by the component (styleable). data-open while the plan is shown, data-busy while a waitUntil promise runs, data-settle="paused | woke" during the shared transition. |
Properties
| Name | 유형 | Description |
|---|---|---|
items | Array<{ id, label, detail?, group: "freeze" | "keep", optional?, kept?, exception?, caveat?, expires?, expiresLabel? }> | Inventory. optional lets the user keep a freeze item running (switch labelled by exception, default “Keep running”), kept is that choice’s initial value; caveat is a fixed warning; expires (date) produces computed caveats before pausing and an automatic “ended while paused” flag on wake. Declarative alternative: <ul data-group="freeze|keep"> children whose <li> carry data-id, data-detail, data-optional, data-kept, data-exception, data-caveat, data-expires, data-expires-label (read once, then removed). Setting it while paused updates what is shown as frozen. |
held | Array<{ id, label, one?, count?, actions? }> | Groups of things held while paused. label is plural (“emails held”), one singular; actions = [{ id, label, done?, tone?: "danger" | "primary", resolves? }] (default Process now / Review / Discard; resolves: false keeps the group open, like Review; danger needs a second press). Declarative alternative: <ul data-group="held"><li data-id data-one data-count data-actions="send:Send now=Sent, review:Review~, discard:Discard!=Discarded"> (=text shown once done, ! = danger, ~ = resolves: false). Read back with resolved action ids. |
state | "awake" | "paused" | "woke" | Current phase (read-only). woke is the wake summary, until Done. |
pausedAt | Date | null | Start of the current pause; settable (like the paused-at attribute) to restore a pause from the server. |
wakeAt | Date | null | Planned wake moment of the current plan or pause; settable (Date, ISO string or "YYYY-MM-DD"; null = manual). |
asleep | number (ms) | Time paused so far (read-only, 0 when not paused). |
strings | Partial<Record<string, string>> | Overrides for every visible text and announcement (English defaults; placeholders such as {subject}, {date}, {relative}, {duration}, {count}, {incoming}). Default action labels (release, review, discard, released, discarded) are overridable here too. |
Methods
| Name | Description |
|---|---|
showPlan() / hidePlan() | Shows or hides the pause plan (awake only), like toggling the open attribute. |
pause() | Pauses with the current plan, exactly like the Pause now button (validation, cancelable mv-pause, waitUntil). Returns Promise<boolean>. |
wakeUp({ reason? }) | Wakes now (reason defaults to "api"): cancelable mv-wake, waitUntil, then the wake summary. Returns Promise<boolean>. |
hold(id, n = 1) | Counts n more things held in a group while paused (n may be negative). Returns the new count, or false when not paused or the group is unknown. Never announced, so it never chatters. |
flag(id, note) | Marks a frozen item that will not (or did not) resume exactly, with the reason; shown under “May not resume exactly” while paused and in the wake summary. flag(id, null) clears it. |
dismiss() | Closes the wake summary (like Done) and returns to awake. |
Events
| Name | Description |
|---|---|
mv-pause | Cancelable, before anything is shown as paused. detail: { freezes: ids, keeps: ids, exceptions: ids, wake: Date | null, incoming: { policy, message?, forwardTo? }, waitUntil(promise) }. Freeze everything in your backend inside waitUntil: the button stays busy meanwhile, a rejection keeps the plan open with an error (mv-pause-error { error }). |
mv-wake | Cancelable, before waking. detail: { reason: "manual" | "scheduled" | "api", pausedAt, wake, asleep (ms), freezes, held: [{ id, count }], waitUntil(promise) }. The promise may resolve to { changed: [{ id, note }] } to flag items that did not resume exactly. Rejection keeps it paused with an error (mv-wake-error { reason, error }). Cancelling a scheduled wake stops it retrying until the plan changes. |
mv-held-action | Cancelable, when a choice is made for a held group in the wake summary. detail: { id, action, count, label, waitUntil(promise) }. Rejection shows an error on that row; success marks the group done (unless the action has resolves: false) and moves focus to the next decision. |
mv-plan-change | Cancelable, when the wake date is changed while paused. detail: { wake: Date | null, previous: Date | null, waitUntil(promise) }. |
mv-dismiss | The wake summary was closed. detail: { unresolved: [{ id, count }] } for held groups left without a decision. |
Content structure
| Name | Description |
|---|---|
(declarative inventory) | Optional children <ul data-group="freeze">, <ul data-group="keep"> and <ul data-group="held"> are read once at first connection and replaced by the component’s own markup (ignored when the items / held properties were set first). |
CSS classes
| Name | Description |
|---|---|
mv-deep-pause-head | Always-visible header: -icon, -title (heading, focus target after pause), -status (dot + word: Active / Paused / Resumed), -sub, -open (“Pause…”). |
mv-deep-pause-plan | Plan section: -inventory with two -col (data-group freeze | keep, -col-head, -col-count), -item rows (data-group, data-exception, -mark, -item-label, -item-detail, -tag, -caveat, -except switch), -options (wake and incoming fieldsets, -subfield, -hint), -error, -footer (-summary, -actions). |
mv-deep-pause-asleep | Paused section: -facts (dl of -fact: time asleep, wake plan, incoming policy), -held with -tile counters, -caveat-box, -details (read-only inventory), -rewake editor. |
mv-deep-pause-review | Wake summary: -group lists of -line (data-tone ok | caution) for resumed and not-exact items, -rows of held groups (-row, data-resolved, data-empty, -row-actions, -row-state). |
CSS variables
| Name | Default | Description |
|---|---|---|
--mv-deep-pause-tone | var(--mv-info) | Paused tint: frozen column and marks, facts icons, breathing dot, card border while paused. |
--mv-deep-pause-ok | var(--mv-success) | Running and resumed color: keeps-running marks, exception tags, resumed checks. |
--mv-deep-pause-caution | var(--mv-warning) | Caveats and items that could not resume exactly. |
--mv-deep-pause-radius | var(--mv-radius-xl) | Card corner radius. |
Accessibility
The card is a plain region of real headings (heading-level, default h3 for the subject, then one and two levels below) and lists: what freezes and what keeps running are two separate <ul>, each labelled by its heading and count, with a distinct icon per list and an “Exception” tag in words, so nothing relies on color. Exceptions are native switches (role="switch") named “Keep running, <item>” through aria-labelledby; toggling one moves the row to the other list and keeps focus on its switch. Wake and incoming choices are native radio groups in fieldsets with legends; the date field has a real label, its min/max, and a hint (aria-describedby) that restates the choice as text (“Wakes on Monday, October 19, in 24 days.”) or explains the error with aria-invalid; the forward field has its own error message. Every date is written in words in the component’s locale, relative days come from Intl.RelativeTimeFormat, and the status is always a word (Active / Paused / Resumed) next to the colored dot. After a pause, focus moves to the title and “<subject> is paused. Wakes on …” is announced politely; after a wake, focus moves to the summary heading and the count of resumed and not-exact items is announced. Focus only moves when it was inside the component (or the action came from its buttons), never when your code pauses or wakes it in the background. Busy buttons get aria-busy and aria-disabled while a waitUntil promise runs; failures appear in role="alert" messages and nothing is shown as paused or awake until it is true. Held counters update silently (read on demand), destructive held choices need a second press on the same button, which is announced (“Press Confirm: discard again to confirm.”) and disarms after 5 seconds; after each decision focus moves to the next one, then to Done. Escape cancels the plan or the wake-date editor and returns focus to the button that opened it. Reduced motion (OS or data-motion="reduce"): no breathing dot, no settle transition, rows move without sliding. Forced colors: borders and the dot use system colors.