Exclusivobeta
Inscribe <mv-inscribe>
Notas críticas que acompañan a un registro allí donde aparezca en la app. Registra notas por id de entidad (cliente, cuenta, dispositivo, paciente, envío, proveedor) con una gravedad (info, caution, critical), un autor, una fecha y una caducidad opcional; cada elemento del ámbito que lleve data-entity="…" (fila de tabla, avatar, @mención, resultado de búsqueda, cabecera de pedido) recibe entonces un marcador compacto, un botón cuya forma indica la gravedad más alta (círculo, triángulo, octógono) junto al recuento, relleno mientras haya una nota crítica sin confirmar. Los elementos añadidos después se detectan automáticamente, las mismas notas se mantienen sincronizadas en todos los lugares donde aparece el registro y las notas caducadas desaparecen solas. Al pasar el puntero o enfocar un marcador se previsualizan las notas y al hacer clic se fijan, con un editor en línea opcional (gravedad, caducidad) cuyas notas nuevas marcan a la vez todos los marcadores de ese registro. Las acciones protegidas (data-entity-guard en un botón o enlace) quedan en espera hasta que se han leído las notas críticas: el primer clic las abre, y el segundo (o “Acknowledge and continue”) registra la confirmación y deja pasar el clic original, con un evento cancelable mv-inscribe-guard y una validez opcional de la confirmación (ack-ttl, "0" = cada vez).
| Categoría | Visualización de datos |
|---|---|
| Tipo | Web Component (<mv-inscribe>) |
| Estado | beta |
| Keywords | exclusive, culture, notes, annotations, entity, crm, support, customer, warning, severity, acknowledgment, guard, confirmation, popover, badge, marker, compliance, handoff |
When to use
- A support, CRM or back-office app must surface “never forget” facts about a customer wherever that customer is shown
- Irreversible actions on a record (delete, refund, ship, discharge) must be held until its critical notes have been read
- The same entity appears in lists, conversations, headers and search results, and its warnings must stay in sync everywhere
- Teams hand off records across shifts and need short, expiring notes attached to the record rather than buried in a history tab
Avoid when
- The content is a one-off rich preview of a link or person, with no notes or severities → use Hover Card instead
- The information is long-form documentation or a discussion thread: markers and popovers suit a few short notes per record
- The warning concerns the whole page or session rather than one record → use Alert instead
Instalación
node scripts/add.mjs inscribe --out ./src/marvelousAgente de IA con el servidor MCP de Marvelous UI: install_components({ slugs: ["inscribe"], target_dir: "<absolute path>/src/marvelous", framework: "react" }).
Archivos copiados (dependencias incluidas): tokens/tokens.css, core/base.css, core/dismiss.js, core/dom.js, core/element.js, core/focus.js, core/observe.js, core/position.js, components/inscribe/inscribe.js, components/inscribe/inscribe.css.
Uso
Marcado de referencia: parte de él y personalízalo con atributos, data-* y variables CSS:
<div id="ins-demo" style="width:min(100%,76rem);margin-inline:auto">
<style>
#ins-demo .ins-console { border:1px solid var(--mv-border); border-radius:var(--mv-radius-xl); background:var(--mv-bg-subtle); overflow:hidden; box-shadow:var(--mv-shadow-sm) }
#ins-demo .ins-bar { display:flex; align-items:center; justify-content:space-between; gap:.75rem 1.5rem; flex-wrap:wrap; padding:.875rem 1.125rem; border-bottom:1px solid var(--mv-border); background:var(--mv-surface) }
#ins-demo .ins-bar h3 { margin:0; font-size:1rem; letter-spacing:-.01em }
#ins-demo .ins-bar p { margin:.125rem 0 0; color:var(--mv-fg-muted); font-size:.75rem }
#ins-demo .ins-controls { display:flex; align-items:center; gap:.5rem 1.25rem; flex-wrap:wrap }
#ins-demo .ins-controls .mv-choice { font-size:.8125rem }
#ins-demo .ins-grid { display:grid; grid-template-columns:18.5rem minmax(0,1fr) 17.5rem; min-height:31rem }
#ins-demo .ins-col { min-width:0; background:var(--mv-surface) }
#ins-demo .ins-col + .ins-col { border-left:1px solid var(--mv-border) }
#ins-demo .ins-h { display:flex; align-items:center; justify-content:space-between; gap:.5rem; margin:0; padding:.75rem 1rem; border-bottom:1px solid var(--mv-border); font-size:.75rem; font-weight:600; color:var(--mv-fg-muted); letter-spacing:.04em; text-transform:uppercase }
#ins-demo .ins-queue ul { list-style:none; margin:0; padding:.375rem; display:grid; gap:.125rem }
#ins-demo .ins-ticket { position:relative; display:grid; gap:.25rem; padding:.625rem .75rem; border-radius:var(--mv-radius-lg) }
#ins-demo .ins-ticket:hover { background:var(--mv-bg-subtle) }
#ins-demo .ins-ticket[aria-current="true"] { background:var(--mv-accent-subtle) }
#ins-demo .ins-t-top { display:flex; align-items:center; justify-content:space-between; gap:.5rem; font-size:.8125rem; font-weight:600 }
#ins-demo .ins-t-top time { color:var(--mv-fg-subtle); font-size:.6875rem; font-weight:500; white-space:nowrap }
#ins-demo .ins-subj { color:var(--mv-fg-muted); font-size:.75rem; line-height:1.35; text-decoration:none; overflow:hidden; text-overflow:ellipsis; white-space:nowrap }
#ins-demo .ins-subj::after { content:""; position:absolute; inset:0; border-radius:inherit }
#ins-demo .ins-subj:focus-visible { outline:none }
#ins-demo .ins-ticket:has(.ins-subj:focus-visible) { box-shadow:var(--mv-focus-ring) }
#ins-demo .ins-ticket .mv-inscribe-marker { position:relative; z-index:1 }
#ins-demo .ins-ticket[data-new] { animation:ins-in .5s var(--mv-ease-out) }
@keyframes ins-in { from { background:color-mix(in oklab, var(--mv-accent) 14%, transparent) } }
#ins-demo .ins-thread { display:grid; grid-template-rows:auto minmax(0,1fr) auto }
#ins-demo .ins-who { display:flex; align-items:center; gap:.75rem; padding:.75rem 1.125rem; border-bottom:1px solid var(--mv-border) }
#ins-demo .ins-who b { font-size:.9375rem }
#ins-demo .ins-who small { display:block; color:var(--mv-fg-muted); font-size:.75rem; margin-top:.0625rem }
#ins-demo .ins-who .mv-badge { margin-left:auto }
#ins-demo .ins-msgs { display:grid; align-content:start; gap:.875rem; padding:1rem 1.125rem }
#ins-demo .ins-msg { display:grid; grid-template-columns:auto minmax(0,1fr); gap:.625rem }
#ins-demo .ins-msg-body { padding:.625rem .75rem; border:1px solid var(--mv-border); border-radius:var(--mv-radius-lg); background:var(--mv-surface); font-size:.8125rem; line-height:1.5 }
#ins-demo .ins-msg-body p { margin:0 }
#ins-demo .ins-msg-meta { display:flex; gap:.5rem; align-items:baseline; margin-bottom:.25rem; font-size:.75rem }
#ins-demo .ins-msg-meta b { font-weight:600 }
#ins-demo .ins-msg-meta span { color:var(--mv-fg-subtle) }
#ins-demo .ins-msg[data-internal] .ins-msg-body { border-style:dashed; background:color-mix(in oklab, var(--mv-warning) 6%, var(--mv-surface)) }
#ins-demo .ins-mention { color:var(--mv-accent); font-weight:600 }
#ins-demo .ins-compose { display:grid; gap:.5rem; padding:.75rem 1.125rem 1rem; border-top:1px solid var(--mv-border) }
#ins-demo .ins-compose textarea { min-height:3.5rem; resize:none }
#ins-demo .ins-compose div { display:flex; justify-content:flex-end; gap:.5rem }
#ins-demo .ins-order { display:grid; align-content:start }
#ins-demo .ins-sec { display:grid; gap:.5rem; padding:.875rem 1rem; border-bottom:1px solid var(--mv-border) }
#ins-demo .ins-sec:last-child { border-bottom:0 }
#ins-demo .ins-row { display:flex; align-items:center; justify-content:space-between; gap:.75rem; font-size:.8125rem }
#ins-demo .ins-row > span:first-child { color:var(--mv-fg-muted) }
#ins-demo .ins-row b { font-weight:600; font-variant-numeric:tabular-nums }
#ins-demo .ins-row a { color:var(--mv-fg); font-weight:500; text-underline-offset:2px }
#ins-demo .ins-oid { font-size:.9375rem; font-weight:650; letter-spacing:-.01em }
#ins-demo .ins-oid small { display:block; color:var(--mv-fg-muted); font-size:.75rem; font-weight:500; letter-spacing:0; margin-top:.125rem }
#ins-demo .ins-total { padding-top:.5rem; border-top:1px dashed var(--mv-border) }
#ins-demo .ins-actions { display:grid; gap:.5rem }
#ins-demo .ins-actions .mv-button { width:100% }
#ins-demo [data-entity-guarded]::before { content:""; width:.75rem; height:.75rem; margin-right:.125rem; background:currentColor; opacity:.9; -webkit-mask:var(--ins-lock) center/contain no-repeat; mask:var(--ins-lock) center/contain no-repeat }
#ins-demo { --ins-lock:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M4.5 7V5a3.5 3.5 0 0 1 7 0v2h.5a1 1 0 0 1 1 1v6a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V8a1 1 0 0 1 1-1zm1.6 0h3.8V5a1.9 1.9 0 0 0-3.8 0z'/%3E%3C/svg%3E") }
#ins-demo .ins-foot { display:flex; align-items:center; gap:.5rem; padding:.5rem 1.125rem; border-top:1px solid var(--mv-border); background:var(--mv-surface); color:var(--mv-fg-muted); font-size:.75rem; min-height:2.25rem }
#ins-demo .ins-foot b { color:var(--mv-fg); font-weight:600 }
#ins-demo .ins-dot { width:.5rem; height:.5rem; border-radius:50%; background:var(--mv-success); flex:none }
#ins-demo .ins-foot[data-tone="block"] .ins-dot { background:var(--mv-danger) }
#ins-demo .ins-foot[data-tone="note"] .ins-dot { background:var(--mv-info) }
#ins-demo .ins-hint { margin:.75rem 0 0; color:var(--mv-fg-muted); font-size:.75rem; text-align:center }
@media (max-width:64rem) {
#ins-demo .ins-grid { grid-template-columns:16rem minmax(0,1fr) }
#ins-demo .ins-order { grid-column:1 / -1; border-left:0; border-top:1px solid var(--mv-border) }
}
@media (max-width:42rem) {
#ins-demo .ins-grid { grid-template-columns:minmax(0,1fr) }
#ins-demo .ins-col + .ins-col { border-left:0; border-top:1px solid var(--mv-border) }
}
</style>
<mv-inscribe id="ins" editable author="Sam Patel">
<div class="ins-console">
<header class="ins-bar">
<div>
<h3>Support console</h3>
<p>Billing & accounts queue · signed in as Sam Patel</p>
</div>
<div class="ins-controls">
<label class="mv-choice" data-control="end">
<input type="checkbox" role="switch" class="mv-switch" id="ins-edit" checked>
<span class="mv-choice-text"><span class="mv-choice-title">Inline editor</span></span>
</label>
<label class="mv-choice" data-control="end">
<input type="checkbox" role="switch" class="mv-switch" id="ins-every">
<span class="mv-choice-text"><span class="mv-choice-title">Acknowledge every time</span></span>
</label>
<button class="mv-button" data-variant="outline" data-size="sm" type="button" id="ins-new">Simulate incoming ticket</button>
</div>
</header>
<div class="ins-grid">
<section class="ins-col ins-queue" aria-labelledby="ins-q-h">
<h4 class="ins-h" id="ins-q-h">Open tickets <span class="mv-badge" data-variant="secondary" data-shape="pill" id="ins-count">5</span></h4>
<ul id="ins-list">
<li class="ins-ticket" aria-current="true">
<span class="ins-t-top"><span data-entity="cust-4821">Diana Okafor</span><time>9:42 AM</time></span>
<a class="ins-subj" href="#ticket-4410">Refund for duplicate charge on INV-20931</a>
</li>
<li class="ins-ticket">
<span class="ins-t-top"><span data-entity="cust-1377">Mateo Álvarez</span><time>9:15 AM</time></span>
<a class="ins-subj" href="#ticket-4407">Can’t update the card on file</a>
</li>
<li class="ins-ticket">
<span class="ins-t-top"><span data-entity="cust-2290">Aiko Tanaka</span><time>8:58 AM</time></span>
<a class="ins-subj" href="#ticket-4402">CSV export drops the last column</a>
</li>
<li class="ins-ticket">
<span class="ins-t-top"><span data-entity="cust-4821">Diana Okafor</span><time>Yesterday</time></span>
<a class="ins-subj" href="#ticket-4388">Please remove our old pilot workspace</a>
</li>
<li class="ins-ticket">
<span class="ins-t-top"><span data-entity="cust-3054">Lars Nygaard</span><time>Yesterday</time></span>
<a class="ins-subj" href="#ticket-4381">Change the billing address on invoices</a>
</li>
</ul>
</section>
<section class="ins-col ins-thread" aria-label="Conversation">
<div class="ins-who">
<span class="mv-avatar" data-variant="tinted" style="--mv-avatar-hue:20" role="img" aria-label="Diana Okafor"><span class="mv-avatar-fallback" aria-hidden="true">DO</span></span>
<div>
<b data-entity="cust-4821">Diana Okafor</b>
<small>[email protected] · Ticket #4410</small>
</div>
<span class="mv-badge" data-variant="danger" data-shape="pill">Urgent</span>
</div>
<div class="ins-msgs">
<div class="ins-msg">
<span class="mv-avatar" data-size="sm" data-variant="tinted" style="--mv-avatar-hue:20" aria-hidden="true"><span class="mv-avatar-fallback">DO</span></span>
<div class="ins-msg-body">
<div class="ins-msg-meta"><b>Diana Okafor</b><span>9:42 AM</span></div>
<p>Hi! We were charged $1,249.00 twice for our annual renewal (INV-20931). Could you refund the duplicate? And while you’re at it, please delete all the data from our old pilot workspace.</p>
</div>
</div>
<div class="ins-msg" data-internal>
<span class="mv-avatar" data-size="sm" data-variant="tinted" style="--mv-avatar-hue:260" aria-hidden="true"><span class="mv-avatar-fallback">PR</span></span>
<div class="ins-msg-body">
<div class="ins-msg-meta"><b>Priya Raman</b><span>Internal note · 9:50 AM</span></div>
<p>Refund is fine. Before anything irreversible, read the notes on <span class="ins-mention" data-entity="cust-4821">@Diana Okafor</span>. Legal still has a hold on this account.</p>
</div>
</div>
</div>
<div class="ins-compose">
<textarea class="mv-textarea" aria-label="Reply to Diana Okafor" placeholder="Reply to Diana…"></textarea>
<div><button class="mv-button" data-size="sm" type="button">Send reply</button></div>
</div>
</section>
<aside class="ins-col ins-order" aria-label="Order details">
<div class="ins-sec">
<div class="ins-oid">Order A-10492<small>Paid Sep 1, 2026 · Card •••• 4417</small></div>
<div class="ins-row"><span>Customer</span><span data-entity="cust-4821">Diana Okafor</span></div>
<div class="ins-row"><span>Account</span><a href="#account-lumen" data-entity="acct-lumen">Lumen Health</a></div>
</div>
<div class="ins-sec">
<div class="ins-row"><span>Team plan · 25 seats, annual</span><b>$1,249.00</b></div>
<div class="ins-row"><span>Duplicate charge</span><b>$1,249.00</b></div>
<div class="ins-row ins-total"><span>Total charged</span><b>$2,498.00</b></div>
</div>
<div class="ins-sec ins-actions">
<button class="mv-button" data-variant="outline" type="button" id="ins-refund">Refund $1,249.00</button>
<button class="mv-button" data-variant="destructive" type="button" id="ins-delete" data-entity-guard="cust-4821">Delete customer data</button>
</div>
</aside>
</div>
<div class="ins-foot" id="ins-status" role="status"><span class="ins-dot" aria-hidden="true"></span><span id="ins-status-text">Every place that shows a customer carries that customer’s notes.</span></div>
</div>
</mv-inscribe>
<p class="ins-hint">Hover or Tab to a marker to read the notes · Click to pin and add one · Try “Delete customer data” · Simulate a ticket: its marker attaches by itself</p>
<script type="module">
const ins = document.getElementById("ins");
await customElements.whenDefined("mv-inscribe");
const $ = (id) => document.getElementById(id);
const now = Date.now(), hour = 36e5, day = 864e5;
ins.entities = {
"cust-4821": { name: "Diana Okafor", kind: "Customer" },
"cust-1377": { name: "Mateo Álvarez", kind: "Customer" },
"cust-2290": { name: "Aiko Tanaka", kind: "Customer" },
"cust-3054": { name: "Lars Nygaard", kind: "Customer" },
"acct-lumen": { name: "Lumen Health", kind: "Account" },
};
ins.notes = [
{ entity: "cust-4821", severity: "critical", author: "Hannah Weiss, Legal", date: now - 21 * day,
text: "Legal hold LH-2291: do not delete, anonymize or export any of this customer’s data until Legal lifts the hold." },
{ entity: "cust-4821", severity: "caution", author: "Marcus Bell", date: now - 36 * day,
text: "Severe peanut allergy: never include food in goodwill gift boxes." },
{ entity: "cust-4821", severity: "info", author: "Priya Raman", date: now - 3 * day, expires: now + 68 * day,
text: "Executive sponsor at Lumen Health. Escalate billing questions to Priya Raman." },
{ entity: "cust-1377", severity: "caution", author: "Payments team", date: now - 5 * day, expires: now + 20 * day,
text: "Open chargeback on the August invoice: route refund requests to Payments before replying." },
{ entity: "cust-2290", severity: "info", author: "Leila Haddad", date: now - 50 * day,
text: "Prefers written follow-ups to calls; usually replies within a day." },
{ entity: "acct-lumen", severity: "caution", author: "Priya Raman", date: now - 2 * hour, expires: now + 40 * day,
text: "Renewal under negotiation: no discounts or credits without Priya’s approval." },
];
const status = (text, tone = "ok") => {
$("ins-status-text").textContent = text;
$("ins-status").dataset.tone = tone;
};
const name = (id) => ins.entities[id]?.name ?? id;
$("ins-edit").addEventListener("change", (e) => { ins.editable = e.target.checked; });
$("ins-every").addEventListener("change", (e) => { ins.ackTtl = e.target.checked ? "0" : null; });
ins.addEventListener("mv-inscribe-guard", (e) => {
const n = e.detail.notes.length;
status(`Held “${e.detail.trigger.textContent.trim()}”: ${n} critical note${n > 1 ? "s" : ""} on ${name(e.detail.entity)} to acknowledge first.`, "block");
});
ins.addEventListener("mv-note-ack", (e) => {
status(`${e.detail.by} acknowledged a ${e.detail.note.severity} note on ${name(e.detail.entity)}.`, "note");
});
ins.addEventListener("mv-note-add", (e) => {
status(`${e.detail.note.author} added a ${e.detail.note.severity} note on ${name(e.detail.entity)}: it now shows everywhere this record appears.`, "note");
});
$("ins-delete").addEventListener("click", () => status("Deletion request queued for Diana Okafor and routed to Legal for review."));
$("ins-refund").addEventListener("click", () => status("Refund of $1,249.00 issued to Diana Okafor’s card ending in 4417."));
$("ins-list").addEventListener("click", (e) => { if (e.target.closest("a[href^='#']")) e.preventDefault(); });
const incoming = [
["cust-4821", "Diana Okafor", "Follow-up: still seeing the duplicate charge"],
["cust-1377", "Mateo Álvarez", "Refund status for order A-10311?"],
["cust-6120", "Kofi Mensah", "How do I add a second admin?"],
];
let next = 0;
$("ins-new").addEventListener("click", () => {
const [id, who, subject] = incoming[next++ % incoming.length];
const li = document.createElement("li");
li.className = "ins-ticket";
li.dataset.new = "";
const top = document.createElement("span");
top.className = "ins-t-top";
const person = document.createElement("span");
person.dataset.entity = id;
person.textContent = who;
const time = document.createElement("time");
time.textContent = "Just now";
top.append(person, time);
const a = document.createElement("a");
a.className = "ins-subj";
a.href = `#ticket-${4411 + next}`;
a.textContent = subject;
li.append(top, a);
$("ins-list").prepend(li);
$("ins-count").textContent = String($("ins-list").children.length);
status(`New ticket from ${who}${ins.notesFor(id).length ? ": their notes came with it" : ""}.`, "note");
});
</script>
</div>Referencia cultural
Memento, Christopher Nolan (2000, película). Un hombre que no puede formar recuerdos nuevos se tatúa en el cuerpo los hechos que nunca debe olvidar y escribe en cada fotografía, para que el conocimiento crítico viaje con la cosa misma, donde no pueda pasarlo por alto. En la interfaz, unas notas breves se asocian al id de un registro y aparecen como marcador en cada elemento que muestra ese registro, y las críticas deben leerse y confirmarse antes de cualquier acción irreversible sobre él.
API
Attributes
| Name | Tipo | Default | Description |
|---|---|---|---|
scope | "self" | "document" | CSS selector | self | Where data-entity and data-entity-guard elements are looked for: inside the element (self), the whole document, or the first element matching the selector. Elements added later are picked up automatically. |
editable | boolean | false | Shows the inline editor (“Add note”: text, severity, expiry) at the bottom of the notes card, and lets data-entity-marker="always" show an empty “+” marker on records without notes. |
author | string | "You" | Name of the current user, used as the author of notes added in the editor and in acknowledgments. |
ack-ttl | duration ("0", "30m", "8h", "7d") | (none) | How long an acknowledgment lasts before guarded actions are held again. Empty = for the life of the page, "0" = every guarded action needs a fresh acknowledgment. Markers turn solid again when an acknowledgment lapses. |
placement | "bottom-start" | "top" | "right" | … | bottom-start | Preferred side of the notes card relative to the marker (flipped and shifted to stay on screen). Guard cards open above the action. |
locale | BCP 47 tag | en-US | Locale of dates, relative times and acknowledgment times. |
data-entity | string | On any element of the scope: the entity id it represents. A marker is attached when that entity has active notes; the element also receives data-inscribed="info | caution | critical" (highest severity) for your own styling. | |
data-entity-name | string | On a data-entity element: human name used in labels when the entities property doesn’t provide one (falls back to the id). | |
data-entity-marker | "none" | "always" | "before" | On a data-entity element: no marker (the element still gets data-inscribed and guards still work), a marker even without notes (with editable, to add the first one), or the marker placed before the content instead of after. | |
data-entity-slot | boolean | On a descendant of a data-entity element: the marker is placed inside it. Otherwise the marker goes after an interactive or void host (link, button, img), into the first cell of a <tr>, or at the end of the host. | |
data-entity-guard | entity id | empty | On a button or link: the action is held while that entity (or, when empty, the closest data-entity ancestor’s) has unacknowledged guarding notes. While armed it carries data-entity-guard-state="armed"; while guarded, data-entity-guarded. |
Properties
| Name | Tipo | Description |
|---|---|---|
notes | Note[] | { [entityId]: Note[] } | All notes. Note = { id?, entity, text, severity: "info" | "caution" | "critical", author?, date? (Date | ISO | ms, default now), expires? (idem), guard? (default: severity === "critical"), acknowledged? { by, at } }. Setting replaces everything; reading returns copies with acknowledged and expired filled in. |
entities | { [entityId]: { name, kind? } | string } | Names (for marker labels and the card header) and kinds (shown under the name, e.g. “Customer”). |
scope / editable / author / ackTtl / placement / locale | reflected | Mirror the attributes. |
Methods
| Name | Description |
|---|---|
inscribe(entityId, note | text) | Adds a note to an entity and returns it (with its generated id); every marker of that entity updates and stamps. No event (the app made the change). |
erase(noteId) | Removes a note. Returns false if it doesn’t exist. |
notesFor(entityId, { includeExpired? }) | Active notes of an entity, most severe then newest first. |
acknowledge(entityId, noteId?) | Records an acknowledgment by author for one note, or for every guarding note of the entity. Returns how many were acknowledged. No event. |
isGuarded(entityId) | True while a guarded action on this entity would be held. |
setEntity(entityId, { name, kind } | name) | Adds or updates one entry of entities. |
open(entityId, anchor?) / close() | Opens the pinned notes card for an entity (anchored to its first marker by default) and moves focus into it; closes it. |
refresh() | Re-resolves the scope and re-scans it. |
Events
| Name | Description |
|---|---|
mv-inscribe-guard | A guarded action was clicked while notes still need acknowledging. detail: { entity, notes, trigger }. Cancelable: preventDefault() lets the click through untouched (e.g. for a role that is allowed to bypass). |
mv-note-ack | A note is being acknowledged from the card or a guarded action. detail: { entity, note, by, at, source: "card" | "guard" }. Cancelable: preventDefault() refuses the acknowledgment and the action stays held. Persist acknowledgments here. |
mv-note-add | A note is being added from the inline editor. detail: { entity, note, source: "editor" }. Cancelable: preventDefault() discards it (e.g. to save it server-side and call inscribe() afterwards). Notes you write count as acknowledged by you. |
mv-inscribe-open | The notes card opened. detail: { entity, mode: "peek" | "pin" | "guard", anchor, notes }. |
mv-inscribe-close | The notes card closed. detail: { entity, reason }. |
Content structure
| Name | Description |
|---|---|
(content) | With scope="self" (default), the region whose data-entity elements are decorated. The element is display: contents; with scope="document" it can sit anywhere, empty. |
CSS classes
| Name | Description |
|---|---|
mv-inscribe-marker | The marker: a <button> (or, inside another link or button, a non-focusable role="img" with data-passive). data-severity="info | caution | critical | none", data-pending while a guarding note is unacknowledged, data-inscribe-open while its card is open. |
mv-inscribe-card | The notes card (role="dialog", Popover API top layer). data-severity (highest), data-mode="peek | pin | guard". Parts: -head, -title, -sub, -guardmsg, -list, -note (data-severity, data-pending), -sev, -text, -meta, -acked, -foot, -form, -btn. |
CSS variables
| Name | Default | Description |
|---|---|---|
--mv-inscribe-info | var(--mv-info) | Tint of info notes (circle). |
--mv-inscribe-caution | var(--mv-warning) | Tint of caution notes (triangle). |
--mv-inscribe-critical | var(--mv-danger) | Tint of critical notes (octagon). |
--mv-inscribe-marker-size | 1.25rem | Marker height. |
--mv-inscribe-width | 22rem | Width of the notes card. |
Accessibility
Each marker is a real <button type="button"> with aria-haspopup="dialog", aria-expanded and aria-controls, named in full (“3 notes on Diana Okafor, 1 critical, 1 caution, needs acknowledgment”); its visible count and shape icon are aria-hidden. Severity never relies on color: the icon shape differs (circle / triangle / octagon), the card spells it out (“Critical”), an unacknowledged critical note makes the marker solid, and forced-colors mode keeps shapes and borders with system colors. Keyboard: focusing a marker with the keyboard previews its notes without moving focus; Enter, Space or ArrowDown pins the card and moves focus into it, Tab from the marker enters the card, Tab past its last control continues to whatever follows the marker in the page, Shift+Tab returns to the marker, and Escape closes the card and returns focus. The card is a labelled non-modal dialog (Popover API top layer, no focus trap) dismissed by Escape or an outside click. Markers are never nested inside other interactive content: inside a link or button the marker becomes a non-focusable role="img" whose label joins the control’s name, and Alt+ArrowDown on that control opens the notes (announced through aria-keyshortcuts). Guarded actions get aria-describedby pointing to a hidden description (“Requires acknowledging 1 critical note on Diana Okafor first: …”) and data-entity-guarded while held; a held click opens the card with focus on “Acknowledge and continue”, the requirement is described by aria-describedby, and Escape or Cancel returns focus to the action. Additions and acknowledgments are announced in a polite live region. Reduced motion (prefers-reduced-motion or data-motion="reduce") removes the stamp, the marker entrance and the card scale; nothing loops.