Exclusivobeta
Time Vault <mv-time-vault>
Una bóveda de mensajes escritos hoy y sellados hasta un momento futuro preciso: una fecha (data-opens, absoluta o relativa como “+1y” tras escribirlos) o una condición con nombre de la app (“cuando lancemos públicamente”, “cuando el MRR supere los 10 000 $”) que tu app alimenta mediante vault.check(state) o el atributo flags, con progreso opcional (0-1 o “7400 $ de 10 000 $”). La lista muestra de cada cápsula el título, el autor, el disparador y un estado sereno en palabras (Sellada, Pronto, Lista para abrir, Abierta) alrededor de un sello cuyo anillo se llena a medida que se acerca el momento, y nunca el contenido. La apertura es una revelación tranquila (el anillo se suelta, la carta se despliega y sus líneas aparecen una a una) que empieza por la procedencia (“Escrita hace 14 meses por Amara Okafor, antes del lanzamiento”); las predicciones que contiene reciben un grupo de radios de veredicto Sí / En parte / No, y una franja de calibración puntúa cada veredicto de toda la bóveda. Un formulario de redacción integrado sella nuevas cápsulas con una fecha o una condición elegida de la lista que proporciona tu app. mv-seal y mv-open son cancelables y aceptan waitUntil(promise), así que el servidor guarda la cápsula y solo entrega su contenido una vez que ha comprobado él mismo el disparador: el componente oculta, no protege.
| Categoría | Visualización de datos |
|---|---|
| Tipo | Web Component (<mv-time-vault>) |
| Estado | beta |
| También instala | button, input, select, textarea |
| Keywords | exclusive, culture, time-capsule, sealed, future-self, letter, retrospective, pre-mortem, predictions, calibration, forecast-review, onboarding, milestone, unlock, scheduled-reveal, trigger, feature-flag, list |
When to use
- A team wants retro notes, pre-mortems or founder letters that only open at launch, a funding round or a year from now
- Predictions made today must be checked later and kept as a record of how well the team called it
- An onboarding letter or contract renewal note should surface exactly when it becomes relevant, with its original context
- A product needs “letters to your future self” that unlock on a date or when an app milestone is reached
Avoid when
- The content is a secret to show once and then destroy, not a message to open later → use Self-Destruct instead
- The point is to forecast a metric from its trend, not to store what people predicted → use Yet To Come instead
- The content must stay confidential until the date: never ship the body to the client, deliver it from mv-open’s waitUntil
Instalación
node scripts/add.mjs time-vault --out ./src/marvelousAgente de IA con el servidor MCP de Marvelous UI: install_components({ slugs: ["time-vault"], target_dir: "<absolute path>/src/marvelous", framework: "react" }).
Archivos copiados (dependencias incluidas): tokens/tokens.css, core/base.css, core/dom.js, core/element.js, core/motion.js, components/time-vault/time-vault.js, components/time-vault/time-vault.css, components/button/button.css, components/input/input.css, components/select/select.css, components/textarea/textarea.css, components/textarea/char-count.js.
Uso
Marcado de referencia: parte de él y personalízalo con atributos, data-* y variables CSS:
<div id="tv-demo" style="width:min(100%,64rem);margin-inline:auto">
<style>
#tv-demo { display:grid; grid-template-columns:minmax(0,1fr) 16.5rem; gap:1.25rem; align-items:start }
#tv-demo .tv-panel { display:grid; gap:1rem; padding:1rem 1.125rem 1.125rem; border:1px solid var(--mv-border); border-radius:var(--mv-radius-xl); background:var(--mv-surface); box-shadow:var(--mv-shadow-sm); font-size:.8125rem }
#tv-demo .tv-panel h4 { margin:0; font-size:.875rem; letter-spacing:-.01em }
#tv-demo .tv-panel p { margin:0; color:var(--mv-fg-muted); font-size:.75rem; line-height:1.5 }
#tv-demo .tv-sep { height:1px; background:var(--mv-border) }
#tv-demo .tv-row { display:flex; align-items:center; justify-content:space-between; gap:.75rem }
#tv-demo .tv-mrr { display:grid; gap:.5rem }
#tv-demo .tv-mrr output { color:var(--mv-fg); font-weight:600; font-variant-numeric:tabular-nums }
#tv-demo .tv-clock { display:grid; gap:.5rem }
#tv-demo .tv-actions { display:flex; gap:.5rem; flex-wrap:wrap }
#tv-demo .tv-log { display:grid; gap:.25rem; min-height:4.5rem; margin:0; padding:0; list-style:none; color:var(--mv-fg-muted); font:.6875rem/1.45 var(--mv-font-mono) }
#tv-demo .tv-log li:first-child { color:var(--mv-fg) }
#tv-demo .mv-choice { font-size:.8125rem }
@media (max-width:52rem) { #tv-demo { grid-template-columns:minmax(0,1fr) } }
</style>
<!-- A product team's vault. The clock is frozen on Sep 24, 2026 so the demo reads the same any day. -->
<mv-time-vault id="tv-vault" label="Team time vault" author="Amara Okafor" era="before launch" now="2026-09-24T10:00">
<data data-condition value="launch">we launch publicly</data>
<data data-condition value="mrr-10k">MRR passes $10k</data>
<data data-condition value="beta-ended">the public beta ends</data>
<data data-condition value="series-a">we close the Series A</data>
<data data-condition value="legacy-api-off">the legacy API is switched off</data>
<template data-capsule data-id="premortem" data-title="Pre-mortem: how the beta could fail" data-author="Priya Raman" data-written="2025-03-10" data-context="during the private beta" data-when="beta-ended" data-opened="2026-08-18" data-expanded>
<p>It is the end of the beta and something went wrong. This is my honest guess at what it was, written while we still had eleven design partners and no pricing page.</p>
<p>If I am right about onboarding, the fix is a guided first project, not more features. If I am wrong, please tell me in the retro.</p>
<ol data-predictions>
<li data-verdict="yes">Onboarding, not pricing, will be the main reason teams churn</li>
<li data-verdict="no">We will need a Windows app before enterprise deals close</li>
<li data-verdict="partly">Support volume will double in the first month after the beta</li>
<li>At least three design partners will become paying customers</li>
</ol>
</template>
<template data-capsule data-id="sofia" data-title="To Sofia, one year after hire" data-author="Daniel Kim" data-written="2025-09-15" data-context="on your first day" data-opens="+1y">
<p>Sofia, welcome. Today you asked more good questions than most people ask in a quarter, so here is a note for the version of you who has been here a year.</p>
<p>I hope you shipped the offline mode you pitched in your interview. If not, I hope you know why, and that the reason taught you something about this team.</p>
<ol data-predictions>
<li>Offline mode will be live for every customer</li>
<li>You will be mentoring at least one new engineer</li>
</ol>
</template>
<template data-capsule data-id="brightwave" data-title="Renewal notes: Brightwave Media" data-author="Marcus Webb" data-written="2025-10-20" data-context="right after signing" data-opens="2026-10-05">
<p>We won Brightwave on the reporting exports, not on price. Their CFO, Hannah Lindqvist, pushed hard for net-60 terms; we gave net-45 and a named support contact.</p>
<ol data-predictions>
<li>They will ask for at least 15% off at renewal</li>
<li>Their usage will have grown past 40 seats</li>
</ol>
</template>
<template data-capsule data-id="mrr" data-title="When we pass $10k MRR" data-author="Amara Okafor" data-written="2025-11-03" data-context="at $2.1k MRR" data-when="mrr-10k">
<p>If you are reading this, the thing we built in a spare room pays for itself. Celebrate first. Then look at which plan got us here, because I bet it is not the one we designed the pricing page around.</p>
<ol data-predictions>
<li>Most of the revenue will come from the Team plan, not Pro</li>
<li>We will still be under 5% monthly churn</li>
</ol>
</template>
<!-- No content in the page: the server hands it over in mv-open, once it has checked the trigger itself. -->
<template data-capsule data-id="launch" data-title="Letter to the launch team" data-author="Amara Okafor" data-written="2025-07-22" data-context="before launch" data-when="launch"></template>
<template data-capsule data-id="2028" data-title="Where we will be in 2028" data-author="Leila Haddad" data-written="2026-01-12" data-context="during the seed round" data-opens="2028-01-01">
<p>A note on what I think the company looks like two years from now.</p>
</template>
</mv-time-vault>
<aside class="tv-panel" aria-labelledby="tv-sim-h">
<div style="display:grid;gap:.25rem">
<h4 id="tv-sim-h">Simulate the app</h4>
<p>Your app tells the vault what is true. Capsules open the moment their condition does.</p>
</div>
<label class="mv-choice" data-control="end">
<input type="checkbox" role="switch" class="mv-switch" id="tv-launch">
<span class="mv-choice-text"><span class="mv-choice-title">Public launch</span><span class="mv-choice-description">Sets the launch flag</span></span>
</label>
<div class="tv-mrr">
<div class="tv-row"><label for="tv-mrr">Monthly recurring revenue</label><output for="tv-mrr" id="tv-mrr-out">$7,400</output></div>
<input type="range" class="mv-slider" id="tv-mrr" min="0" max="12000" step="100" value="7400">
</div>
<div class="tv-sep"></div>
<div class="tv-clock">
<div class="tv-row"><span>Today</span><span class="mv-badge" data-variant="secondary" id="tv-today">Sep 24, 2026</span></div>
<div class="tv-actions">
<button type="button" class="mv-button" data-variant="outline" data-size="sm" id="tv-jump">Jump 2 weeks ahead</button>
<button type="button" class="mv-button" data-variant="ghost" data-size="sm" id="tv-reset">Reset</button>
</div>
</div>
<div class="tv-sep"></div>
<div style="display:grid;gap:.5rem">
<h4>Events</h4>
<ul class="tv-log" id="tv-log" aria-live="polite"><li>Vault loaded.</li></ul>
</div>
</aside>
<script type="module">
const vault = document.getElementById("tv-vault");
const launch = document.getElementById("tv-launch");
const mrr = document.getElementById("tv-mrr");
const mrrOut = document.getElementById("tv-mrr-out");
const today = document.getElementById("tv-today");
const log = document.getElementById("tv-log");
const START = new Date(2026, 8, 24, 10).getTime();
const usd = (n) => n.toLocaleString("en-US", { style: "currency", currency: "USD", maximumFractionDigits: 0 });
const short = (n) => `$${(n / 1000).toFixed(1).replace(/\.0$/, "")}k`;
const say = (text) => {
log.prepend(Object.assign(document.createElement("li"), { textContent: text }));
while (log.children.length > 4) log.lastElementChild.remove();
};
const syncMrr = () => {
const v = Number(mrr.value);
mrrOut.textContent = usd(v);
vault.check({ "mrr-10k": { progress: v / 10000, text: `${short(v)} of $10k` } });
};
const syncClock = () => {
today.textContent = vault.now.toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" });
};
// The "server": content for the launch letter is only released on open.
const LETTER = {
body: "To everyone who shipped this: I am writing from the week we almost postponed. We had no status page, two open security findings and a landing page nobody liked.\n\nWhatever launch day looked like, remember that the version we were scared of was the one people wanted. Thank you for betting your year on it.",
predictions: [
"The first week will bring more than 300 sign-ups",
"Someone will find a billing bug within 48 hours",
"Our first public review will mention the keyboard shortcuts",
],
};
vault.addEventListener("mv-open", (e) => {
const { id, title } = e.detail.capsule;
say(`Opening “${title}”…`);
const wait = new Promise((resolve) => setTimeout(resolve, 650));
e.detail.waitUntil((id === "launch" ? wait.then(() => LETTER) : wait).then((content) => { say(`Opened “${title}”.`); return content; }));
});
vault.addEventListener("mv-ready", (e) => say(`Ready: “${e.detail.capsule.title}” (${e.detail.reason}).`));
vault.addEventListener("mv-seal", (e) => {
say(`Sealing “${e.detail.capsule.title}”…`);
e.detail.waitUntil(new Promise((resolve) => setTimeout(resolve, 500)).then(() => say(`Sealed “${e.detail.capsule.title}”.`)));
});
vault.addEventListener("mv-verdict", (e) => {
const c = e.detail.calibration;
say(`Verdict: ${e.detail.verdict}. Calibration ${Math.round(c.score * 100)}%.`);
});
launch.addEventListener("change", () => {
vault.flags = launch.checked ? "beta-ended launch" : "beta-ended";
say(launch.checked ? "Launch flag set." : "Launch flag cleared.");
});
mrr.addEventListener("input", syncMrr);
document.getElementById("tv-jump").addEventListener("click", () => {
vault.now = vault.now.getTime() + 14 * 864e5;
syncClock();
say(`Clock moved to ${today.textContent}.`);
});
document.getElementById("tv-reset").addEventListener("click", () => {
vault.now = START;
launch.checked = false;
vault.flags = "beta-ended";
mrr.value = "7400";
syncMrr();
syncClock();
say("Clock and flags reset.");
});
vault.flags = "beta-ended";
syncMrr();
</script>
</div>Referencia cultural
Fundación, Isaac Asimov (1951, libro). Un científico muerto hace mucho tiempo graba mensajes que una bóveda sellada solo reproduce en los momentos de crisis precisos que él predijo, cada uno dirigido exactamente a esa situación y revelando lo bien que la previó. Aquí, las cápsulas escritas hoy permanecen selladas hasta que se cumple su fecha o su condición de la app, luego se abren indicando quién las escribió y cuándo, y sus predicciones se juzgan acertadas, en parte acertadas o erróneas para construir un historial de calibración.
API
Attributes
| Name | Tipo | Default | Description |
|---|---|---|---|
author | string | Name stamped on capsules sealed with the compose form. Without it the form shows a “From” field. | |
era | string | Phrase describing the present moment (“before launch”, “during the private beta”), stored as the context of every capsule sealed now and read back when it opens: “Written 14 months ago by Amara Okafor, before launch”. | |
flags | space- or comma-separated condition ids | Conditions that are currently true, for apps that prefer an attribute to check(). Adding an id unseals every capsule waiting on it (mv-ready). | |
now | ISO date or datetime | Freezes the clock used by date triggers and relative texts (previews, tests, server-rendered snapshots). Without it the real time is used and the component wakes itself at the next opening or due-soon boundary. | |
soon | period ("30d", "2w", "3mo") | 30d | A dated capsule is “Due soon” when it opens within this window. |
soon-progress | number (0..1) | 0.8 | A condition capsule is “Due soon” once its reported progress reaches this share. |
label | string | Time vault | Visible heading of the vault (also names the list). |
heading-level | number (1..5) | 3 | Level of the vault heading; capsule titles and the compose title use the next level. |
locale | BCP 47 tag | en-US | Locale of dates and relative times (Intl.DateTimeFormat / RelativeTimeFormat). |
sort | auto | none | auto | auto orders ready, due soon, sealed (soonest first, then conditions by progress) and opened (newest first), with a short FLIP when a capsule moves. none keeps your order. |
readonly | boolean | Hides “New capsule” and shows verdicts without letting them change (an archive, a shared read-only view). | |
<template data-capsule> | markup | One capsule per template (inert: nothing renders before opening). data-title, data-author, data-written, data-context, data-opens (date or "+1y" / "6mo" / "90d" after data-written) or data-when (condition id), data-opened (already opened on that date), data-expanded, data-id. Content = the letter; an inner <ol data-predictions> lists the predictions (li data-verdict="yes | partly | no"). Read once, then removed from the DOM. | |
<data data-condition value> | markup | One named condition offered by the compose form: value = id, text = label, phrased to follow “Opens when…” (“we launch publicly”). | |
data-state (on capsules) | sealed | soon | ready | opened | Set by the component on each .mv-time-vault-capsule, with data-trigger (date | condition), data-fresh (just unsealed), data-pending (content loading) and data-revealing. |
Properties
| Name | Tipo | Description |
|---|---|---|
capsules | Array<{ id?, title, author?, written?, context?, opens? | when?, body?: string | Node, predictions?: Array<string | { id?, text, verdict? }>, opened?, expanded?, data? }> | Read: every capsule as plain data with its current state. Write: replaces the list. body is never rendered before opening; omit it and deliver it from mv-open for real sealing. Strings are split into paragraphs on blank lines. |
conditions | Array<{ id, label }> | Named conditions the compose form offers and the rows use to phrase their trigger (“Opens when {label}”). |
calibration | { yes, partly, no, pending, judged, total, score } | Read-only score over the predictions of opened capsules; score = (yes + partly / 2) / judged, null before the first verdict. |
now | Date | number | string | null | Clock override (same as the attribute; null returns to real time). |
strings | Partial<Record<string, string>> | Overrides for every visible text and announcement (states, triggers, provenance “Written {ago} by {author}{context}”, form labels and errors, verdict labels, calibration). English defaults. |
Methods
| Name | Description |
|---|---|
check(state) | Feeds condition triggers: { launch: true, "mrr-10k": 0.74, renewal: { progress: 0.5, text: "$4.1k of $8k" } }. true, progress ≥ 1 or met: true unseals; a number or progress below 1 drives the ring and “Due soon”. Returns the ids that became ready. |
add(capsule) | Adds a capsule without the form and without mv-seal (data coming from your server). Returns its public data. |
remove(id) | Removes a capsule. |
unseal(id) | Makes a capsule ready whatever its trigger says (your server decided). Emits mv-ready. |
open(id) | Opens a ready capsule with the reveal, as if “Open” was clicked (emits mv-open). Resolves true once revealed. |
toggle(id, force?) | Expands or collapses an opened capsule. |
showCompose() / hideCompose() | Shows or hides the compose form. |
Events
| Name | Description |
|---|---|
mv-seal | Cancelable, when the compose form is submitted and valid. detail: { capsule, waitUntil(promise) }. Persist it server-side inside waitUntil: the button shows “Sealing…”, a rejection keeps the form with an error, a resolved { id, data } replaces the local id. preventDefault() refuses the capsule. |
mv-ready | Cancelable, a capsule’s trigger just became true. detail: { capsule, reason: "date" | "condition" | "api" }. preventDefault() keeps it sealed (until the next check() of its condition, or unseal()). |
mv-open | Cancelable, a ready capsule is about to be revealed. detail: { capsule, waitUntil(promise) }. Fetch the content from your server here: resolve a string, a Node or { body, predictions }. The ring spins while it loads; a rejection keeps it ready and announces the failure. |
mv-verdict | A prediction received a verdict. detail: { capsule, prediction, verdict: "yes" | "partly" | "no", calibration }. |
Content structure
| Name | Description |
|---|---|
(content) | Optional <data data-condition> and <template data-capsule> children, read at first connection and removed. For framework-rendered lists prefer the capsules and conditions properties. |
CSS classes
| Name | Description |
|---|---|
mv-time-vault-head | Header: .mv-time-vault-mark, .mv-time-vault-heading, .mv-time-vault-summary (“3 sealed · 1 ready to open · 1 opened”), .mv-time-vault-new. |
mv-time-vault-calibration | Calibration strip: -cal-label, -cal-score, -cal-bar with -cal-seg[data-kind=yes|partly|no|pending], -cal-text. Hidden until an opened capsule has predictions. |
mv-time-vault-compose | The compose form: -field, -label, -hint, -pred-fields, -seg (trigger kind), -when-pane, -quick, -error, -compose-actions. |
mv-time-vault-capsule | One list item: .mv-time-vault-row (.mv-time-vault-emblem with -track, -arc and -glyph, .mv-time-vault-title, -byline, -when, .mv-time-vault-status, .mv-time-vault-action) and .mv-time-vault-letter. |
mv-time-vault-letter | The opened content: -letter-head (-provenance, -details), -body, -predictions (-pred-head, -pred-list, .mv-time-vault-prediction[data-verdict] with -claim and .mv-time-vault-verdict radio group of .mv-time-vault-choice). |
CSS variables
| Name | Default | Description |
|---|---|---|
--mv-time-vault-seal | var(--mv-fg-subtle) | Ring and seal color of sealed capsules. |
--mv-time-vault-soon | var(--mv-warning) | Tone of “Due soon” capsules. |
--mv-time-vault-ready | var(--mv-accent) | Tone of ready capsules, their edge, breathing seal and reveal. |
--mv-time-vault-yes / -partly / -no | var(--mv-success) / var(--mv-warning) / var(--mv-danger) | Verdict colors (choices, markers, calibration bar). |
--mv-time-vault-emblem-size | 2.5rem | Diameter of the seal emblem (the letter aligns with the text column after it). |
Accessibility
The vault is a labelled list (the heading names it) and every capsule is a list item with a real heading, so screen-reader users can jump between capsules. State is always spelled out in text (“Sealed”, “Due soon”, “Ready to open”, “Opened”) next to the trigger in words (“Opens Oct 5, 2026 · in 11 days”, “Opens when MRR passes $10k · $7.4k of $10k”); the seal and its ring are aria-hidden decoration, and sealed content is simply not in the DOM (templates are inert and removed after reading). The Open / Read / Hide button names its capsule (“Open ‘To Sofia, one year after hire’”), is described by the trigger line, and as a disclosure carries aria-expanded and aria-controls pointing to the letter, a labelled region. A polite live region announces when a capsule becomes ready while the page is open, when one is revealed (“‘Letter to the launch team’ is open. Written 14 months ago by Amara Okafor, before launch.”), a failed opening and a new seal; focus stays on the button when opening and moves to the new capsule after sealing. Each prediction’s verdict is a native radio group in a fieldset whose legend asks “Was it right?” and whose description is the claim: Tab reaches it, arrow keys choose Yes / Partly / No, and the claim, not color alone, carries the result (a filled marker, a strike-through for “No”). The compose form uses real labels, a labelled radio group for the trigger kind, aria-invalid with an alert error message and focus on the first invalid field, and Escape closes it back to “New capsule”. Reduced motion (OS or data-motion="reduce"): no unfolding, no staggered lines, no breathing or spinning seal, no reordering glide; everything appears in place. Forced colors: rings, markers and the selected verdict use system colors.