Exclusivobeta
Field Guide <mv-field-guide>
Cuadrícula de colección numerada en la que cada entrada tiene tres niveles de conocimiento, para que lo que falta sea tan visible como lo conseguido: desconocido (un hueco numerado vacío con contorno discontinuo y una pista opcional), visto (la ilustración reducida a una silueta de un solo tono, el nombre, los detalles bloqueados y una pista de “cómo conseguirlo”) y obtenido (tarjeta a todo color con resumen, descripción, datos y una llamada a la acción). Los saltos en la numeración se convierten en huecos sin nombre, para que el usuario vea los vacíos de la serie. Subir de nivel se anima en su sitio: la silueta surge de un desenfoque al descubrirla y luego la ilustración completa se rellena desde abajo al obtenerla, con un pulso único (un simple fundido con movimiento reducido). Una cabecera de resumen muestra “12 de 30 usados” con una barra segmentada (obtenidos / vistos / desconocidos, distinguidos por la forma además del color) y píldoras de filtro con recuentos; un panel de detalle fijo sigue a la entrada con el foco. La cuadrícula es un listbox real con navegación 2D con flechas por fila visual, Home/End, PageUp/PageDown y búsqueda por escritura en los nombres; mark(id, state) la controla y emite los cancelables mv-discover y mv-own, el progreso puede persistir en localStorage (el conocimiento nunca retrocede al recargar), y las etiquetas de estado se adaptan al dominio (Usado, Conseguido, Completado…).
| Categoría | Visualización de datos |
|---|---|
| Tipo | Web Component (<mv-field-guide>) |
| Estado | beta |
| También instala | button |
| Keywords | exclusive, culture, collection, discovery, feature-discovery, achievements, badges, gamification, progress, onboarding, curriculum, unlock, locked, silhouette, listbox, grid, keyboard, persist |
When to use
- A SaaS app wants to show which features a team has used, which it has only heard about and which remain to find
- Achievements, badges or collectibles where locked items should tease their shape and tell users how to earn them
- A course or learning path where lessons are locked, unlocked or completed and progress must read at a glance
- A developer portal tracking which API endpoints or SDKs someone has tried, with a how-to for each untried one
Avoid when
- The items must be done in a fixed order, one after another, like a checkout or a setup wizard → use Stepper instead
- Only a single completion percentage matters and individual items don't need to be browsed → use Progress instead
- The list is a marketing overview of features for prospects, not a record of one user's progress → use Features instead
Instalación
node scripts/add.mjs field-guide --out ./src/marvelousAgente de IA con el servidor MCP de Marvelous UI: install_components({ slugs: ["field-guide"], 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/focus.js, components/field-guide/field-guide.js, components/field-guide/field-guide.css, components/button/button.css.
Uso
Marcado de referencia: parte de él y personalízalo con atributos, data-* y variables CSS:
<div id="fgd-demo" style="width:min(100%,68rem);margin-inline:auto">
<style>
#fgd-demo { display:grid; gap:1.25rem; align-content:start }
#fgd-demo .fgd-card { border:1px solid var(--mv-border); border-radius:var(--mv-radius-xl); background:var(--mv-surface); box-shadow:var(--mv-shadow-sm); overflow:hidden }
#fgd-demo .fgd-top { 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) }
#fgd-demo .fgd-top h3 { display:flex; align-items:center; gap:.5rem; margin:0; font-size:1rem; letter-spacing:-.01em }
#fgd-demo .fgd-top p { margin:.125rem 0 0; color:var(--mv-fgd-muted); font-size:.8125rem }
#fgd-demo .fgd-inner { padding:1.25rem }
#fgd-demo .fgd-controls { display:flex; align-items:center; justify-content:space-between; gap:.75rem 1rem; flex-wrap:wrap; padding:.875rem 1.25rem; border-top:1px solid var(--mv-border); background:var(--mv-bg-subtle) }
#fgd-demo .fgd-actions { display:flex; align-items:center; gap:.5rem; flex-wrap:wrap }
#fgd-demo .fgd-k { color:var(--mv-fgd-muted); font-size:.75rem; font-weight:600 }
#fgd-demo .fgd-log { flex:1 1 16rem; min-height:1.25rem; margin:0; color:var(--mv-fgd-subtle); font:.75rem/1.4 var(--mv-font-mono); text-align:end }
#fgd-demo .fgd-choice { font-size:.8125rem }
</style>
<!-- Icon sprite: symbols inherit stroke styling from the component -->
<svg width="0" height="0" style="position:absolute" aria-hidden="true" focusable="false">
<symbol id="fgd-i-dash" viewBox="0 0 24 24"><rect x="3.5" y="3.5" width="7" height="8" rx="1.5"/><rect x="13.5" y="3.5" width="7" height="5" rx="1.5"/><rect x="13.5" y="11.5" width="7" height="9" rx="1.5"/><rect x="3.5" y="14.5" width="7" height="6" rx="1.5"/></symbol>
<symbol id="fgd-i-pulse" viewBox="0 0 24 24"><path d="M3 12h4l2.5-6.5 5 13L17 12h4"/></symbol>
<symbol id="fgd-i-funnel" viewBox="0 0 24 24"><path d="M3.5 4.5h17l-6.5 8v6l-4 2v-8z"/></symbol>
<symbol id="fgd-i-retention" viewBox="0 0 24 24"><path d="M4 4v16h16"/><path d="M7.5 8.5c2 5 5.5 7.5 11 8"/></symbol>
<symbol id="fgd-i-cohort" viewBox="0 0 24 24"><circle cx="9" cy="8.5" r="3.5"/><path d="M2.5 20a6.5 6.5 0 0 1 13 0"/><path d="M16 5a3.5 3.5 0 0 1 0 7"/><path d="M18.5 14.5A6.5 6.5 0 0 1 21.5 20"/></symbol>
<symbol id="fgd-i-replay" viewBox="0 0 24 24"><rect x="3" y="5" width="18" height="14" rx="2.5"/><path d="M10 9.25v5.5L14.75 12z"/></symbol>
<symbol id="fgd-i-bell" viewBox="0 0 24 24"><path d="M6 16.5V11a6 6 0 0 1 12 0v5.5l1.5 2h-15z"/><path d="M10 21h4"/></symbol>
<symbol id="fgd-i-calendar" viewBox="0 0 24 24"><rect x="3.5" y="5" width="17" height="15.5" rx="2.5"/><path d="M3.5 10h17M8 3v4M16 3v4"/></symbol>
<symbol id="fgd-i-flame" viewBox="0 0 24 24"><path d="M12 3c.8 3.2 5 5.4 5 10a5 5 0 0 1-10 0c0-2.2 1-3.6 2.2-4.6.2 1.8 1 2.8 2.3 3.1C11 8.8 10.8 6 12 3z"/></symbol>
<symbol id="fgd-i-flag" viewBox="0 0 24 24"><path d="M5.5 21V4"/><path d="M5.5 4.5h12l-2.5 4 2.5 4h-12"/></symbol>
<symbol id="fgd-i-beaker" viewBox="0 0 24 24"><path d="M9 3.5h6M10 3.5v6l-5.2 8.6A2 2 0 0 0 6.5 21h11a2 2 0 0 0 1.7-2.9L14 9.5v-6"/><path d="M7.5 15h9"/></symbol>
<symbol id="fgd-i-db" viewBox="0 0 24 24"><ellipse cx="12" cy="5.5" rx="7" ry="2.5"/><path d="M5 5.5v13c0 1.4 3.1 2.5 7 2.5s7-1.1 7-2.5v-13"/><path d="M5 12c0 1.4 3.1 2.5 7 2.5s7-1.1 7-2.5"/></symbol>
<symbol id="fgd-i-link" viewBox="0 0 24 24"><path d="M9.5 17H7a5 5 0 0 1 0-10h2.5"/><path d="M14.5 7H17a5 5 0 0 1 0 10h-2.5"/><path d="M8 12h8"/></symbol>
<symbol id="fgd-i-sync" viewBox="0 0 24 24"><path d="M19.5 10.5A7.5 7.5 0 0 0 6 6.8L4.5 8.5"/><path d="M4.5 4.5v4h4"/><path d="M4.5 13.5A7.5 7.5 0 0 0 18 17.2l1.5-1.7"/><path d="M19.5 19.5v-4h-4"/></symbol>
<symbol id="fgd-i-note" viewBox="0 0 24 24"><path d="M4 5.5A1.5 1.5 0 0 1 5.5 4h13A1.5 1.5 0 0 1 20 5.5v9a1.5 1.5 0 0 1-1.5 1.5H9l-5 4z"/></symbol>
<symbol id="fgd-i-share" viewBox="0 0 24 24"><circle cx="17.5" cy="5.5" r="2.5"/><circle cx="6.5" cy="12" r="2.5"/><circle cx="17.5" cy="18.5" r="2.5"/><path d="m8.7 10.8 6.6-4M8.7 13.2l6.6 4"/></symbol>
<symbol id="fgd-i-shield" viewBox="0 0 24 24"><path d="M12 3 5 6v5.5c0 4.3 3 7.9 7 9.5 4-1.6 7-5.2 7-9.5V6z"/></symbol>
<symbol id="fgd-i-key" viewBox="0 0 24 24"><circle cx="8" cy="15.5" r="4"/><path d="m11 12.5 8.5-8.5M16.5 7l2.5 2.5"/></symbol>
<symbol id="fgd-i-file" viewBox="0 0 24 24"><path d="M14 3.5H7A1.5 1.5 0 0 0 5.5 5v14A1.5 1.5 0 0 0 7 20.5h10a1.5 1.5 0 0 0 1.5-1.5V8z"/><path d="M14 3.5V8h4.5M9 13h6M9 16.5h4"/></symbol>
<symbol id="fgd-i-code" viewBox="0 0 24 24"><path d="m8 8-4 4 4 4M16 8l4 4-4 4M13.5 5l-3 14"/></symbol>
<symbol id="fgd-i-phone" viewBox="0 0 24 24"><rect x="7" y="2.5" width="10" height="19" rx="2.5"/><path d="M11 18.5h2"/></symbol>
<symbol id="fgd-i-spark" viewBox="0 0 24 24"><path d="m11 3 1.9 5.1L18 10l-5.1 1.9L11 17l-1.9-5.1L4 10l5.1-1.9z"/><path d="m18.5 15 .8 2.2 2.2.8-2.2.8-.8 2.2-.8-2.2-2.2-.8 2.2-.8z"/></symbol>
</svg>
<!-- Main: feature discovery in a product analytics app -->
<section class="fgd-card" aria-label="Feature discovery">
<div class="fgd-top">
<div>
<h3>Your team in Lumen <span class="mv-badge" data-variant="info" data-shape="pill">Northwind Labs</span></h3>
<p>What your workspace uses, what it has seen and what is still waiting to be found.</p>
</div>
<label class="mv-choice fgd-choice" data-control="end">
<input type="checkbox" role="switch" class="mv-switch" id="fgd-panel" checked>
<span class="mv-choice-text"><span class="mv-choice-title">Details panel</span></span>
</label>
</div>
<div class="fgd-inner">
<mv-field-guide id="fgd-main" label="Lumen features" owned-label="Used" total="24" selected="webhooks">
<div data-entry="dashboards" data-number="1" data-state="owned" data-name="Dashboards" data-icon="#fgd-i-dash" data-summary="42 dashboards · used today" data-description="Pin charts, tables and notes to shared boards that refresh every five minutes." data-href="#dashboards"></div>
<div data-entry="events" data-number="2" data-state="owned" data-name="Event tracking" data-icon="#fgd-i-pulse" data-summary="3.2M events this month" data-href="#events"></div>
<div data-entry="funnels" data-number="3" data-state="owned" data-name="Funnels" data-icon="#fgd-i-funnel" data-summary="Used 2 days ago" data-href="#funnels"></div>
<div data-entry="retention" data-number="4" data-state="owned" data-name="Retention" data-icon="#fgd-i-retention" data-summary="Used last week" data-href="#retention"></div>
<div data-entry="cohorts" data-number="5" data-state="seen" data-name="Cohorts" data-icon="#fgd-i-cohort" data-hint="Save a segment from any chart to turn it into a cohort." data-href="#cohorts"></div>
<div data-entry="session-replay" data-number="6" data-state="seen" data-name="Session replay" data-icon="#fgd-i-replay" data-hint="Install the web SDK v4 and turn on recording." data-href="#replay" data-cta="Set up recording" data-summary="First replay watched today" data-description="Watch real sessions with clicks, scrolls and rage-clicks highlighted, linked to every funnel step."></div>
<div data-entry="alerts" data-number="7" data-state="owned" data-name="Alerts" data-icon="#fgd-i-bell" data-summary="6 alerts · last fired Sep 21" data-href="#alerts"></div>
<div data-entry="reports" data-number="8" data-state="owned" data-name="Scheduled reports" data-icon="#fgd-i-calendar" data-summary="Weekly to 14 people" data-href="#reports"></div>
<div data-entry="heatmaps" data-number="9" data-state="unknown" data-name="Heatmaps" data-icon="#fgd-i-flame" data-hint="Hidden in the page menu of any chart." data-href="#heatmaps" data-cta="Open a heatmap"></div>
<div data-entry="flags" data-number="10" data-state="seen" data-name="Feature flags" data-icon="#fgd-i-flag" data-hint="Create a flag from Settings, then Experiments." data-href="#flags"></div>
<div data-entry="ab-tests" data-number="12" data-state="unknown" data-name="A/B tests" data-icon="#fgd-i-beaker" data-hint="Unlocks after your first feature flag."></div>
<div data-entry="sql" data-number="13" data-state="owned" data-name="SQL editor" data-icon="#fgd-i-db" data-summary="118 saved queries" data-href="#sql">
<dl data-details class="mv-field-guide-facts">
<div><dt>Queries this month</dt><dd>1,284</dd></div>
<div><dt>Median run time</dt><dd>1.8 s</dd></div>
</dl>
</div>
<div data-entry="webhooks" data-number="14" data-state="seen" data-name="Webhooks" data-icon="#fgd-i-link" data-hint="Add an endpoint in Settings, then Developers, and send a test event." data-href="#webhooks" data-cta="Set up webhooks" data-summary="1 endpoint · 200 OK" data-description="Stream events to your own servers in real time, signed and retried for 72 hours."></div>
<div data-entry="warehouse" data-number="15" data-state="unknown" data-name="Warehouse sync" data-icon="#fgd-i-sync" data-hint="Available on the Scale plan."></div>
<div data-entry="annotations" data-number="16" data-state="owned" data-name="Annotations" data-icon="#fgd-i-note" data-summary="Used yesterday" data-href="#annotations"></div>
<div data-entry="shared-links" data-number="17" data-state="owned" data-name="Shared links" data-icon="#fgd-i-share" data-summary="9 public links" data-href="#links"></div>
<div data-entry="roles" data-number="18" data-state="seen" data-name="Roles & permissions" data-icon="#fgd-i-shield" data-hint="Invite a teammate as Viewer to try custom roles." data-href="#roles"></div>
<div data-entry="sso" data-number="19" data-state="unknown" data-name="Single sign-on" data-icon="#fgd-i-key" data-hint="Available on the Enterprise plan."></div>
<div data-entry="audit-log" data-number="20" data-state="unknown" data-name="Audit log" data-icon="#fgd-i-file"></div>
<div data-entry="api-keys" data-number="21" data-state="owned" data-name="API keys" data-icon="#fgd-i-code" data-summary="3 active keys" data-href="#keys"></div>
<div data-entry="mobile-sdk" data-number="23" data-state="seen" data-name="Mobile SDK" data-icon="#fgd-i-phone" data-hint="Add the iOS or Android package and send one event." data-href="#mobile"></div>
<div data-entry="ai-insights" data-number="24" data-state="unknown" data-name="AI insights" data-icon="#fgd-i-spark" data-hint="Rolling out this fall."></div>
</mv-field-guide>
</div>
<div class="fgd-controls">
<span class="fgd-actions">
<span class="fgd-k">Simulate:</span>
<button type="button" class="mv-button" data-variant="outline" data-size="sm" id="fgd-discover">Open a heatmap</button>
<button type="button" class="mv-button" data-variant="outline" data-size="sm" id="fgd-webhook">Send a test webhook</button>
<button type="button" class="mv-button" data-variant="outline" data-size="sm" id="fgd-replay">Watch a replay</button>
<button type="button" class="mv-button" data-variant="ghost" data-size="sm" id="fgd-reset">Reset</button>
</span>
<p class="fgd-log" id="fgd-log" aria-live="polite">Arrow keys move through the grid · type a name to jump</p>
</div>
</section>
<!-- Compact: a learning path without the side panel -->
<section class="fgd-card" aria-label="Learning path">
<div class="fgd-top">
<div>
<h3>SQL for analysts</h3>
<p>Lessons unlock as you go; locked ones say what opens them.</p>
</div>
</div>
<div class="fgd-inner">
<mv-field-guide id="fgd-path" label="Learning path" owned-label="Completed" seen-label="Unlocked" unknown-label="Locked" density="compact" detail="none" filters="false">
<div data-entry="select" data-state="owned" data-name="SELECT basics" data-icon="#fgd-i-db" data-summary="12 min"></div>
<div data-entry="where" data-state="owned" data-name="Filtering rows" data-icon="#fgd-i-funnel" data-summary="15 min"></div>
<div data-entry="order" data-state="owned" data-name="Sorting" data-icon="#fgd-i-retention" data-summary="8 min"></div>
<div data-entry="group" data-state="owned" data-name="Aggregates" data-icon="#fgd-i-dash" data-summary="20 min"></div>
<div data-entry="joins" data-state="seen" data-name="Joins" data-icon="#fgd-i-link" data-hint="Next up · 25 min"></div>
<div data-entry="subqueries" data-state="seen" data-name="Subqueries" data-icon="#fgd-i-code" data-hint="18 min"></div>
<div data-entry="dates" data-state="unknown" data-name="Dates & time" data-icon="#fgd-i-calendar" data-hint="After Joins"></div>
<div data-entry="windows" data-state="unknown" data-name="Window functions" data-icon="#fgd-i-pulse" data-hint="After Subqueries"></div>
<div data-entry="ctes" data-state="unknown" data-name="CTEs" data-icon="#fgd-i-file"></div>
<div data-entry="perf" data-state="unknown" data-name="Query performance" data-icon="#fgd-i-flame"></div>
<div data-entry="capstone" data-state="unknown" data-name="Capstone project" data-icon="#fgd-i-spark" data-hint="Final lesson"></div>
</mv-field-guide>
</div>
</section>
<script type="module">
const guide = document.getElementById("fgd-main");
await customElements.whenDefined("mv-field-guide");
const $ = (id) => document.getElementById(id);
const log = $("fgd-log");
const initial = guide.state;
const say = (text) => { log.textContent = text; };
const sync = () => {
const s = guide.state;
$("fgd-discover").disabled = s.heatmaps !== "unknown";
$("fgd-webhook").disabled = s.webhooks === "owned";
$("fgd-replay").disabled = s["session-replay"] === "owned";
};
guide.addEventListener("mv-discover", (e) => say(`mv-discover · ${e.detail.name}`));
guide.addEventListener("mv-own", (e) => say(`mv-own · ${e.detail.name} (from ${e.detail.from})`));
guide.addEventListener("mv-select", (e) => say(`mv-select · entry ${e.detail.number} (${e.detail.state})`));
guide.addEventListener("mv-change", sync);
guide.addEventListener("click", (e) => { if (e.target.closest("a[href^='#']")) e.preventDefault(); });
$("fgd-discover").addEventListener("click", () => { guide.select("heatmaps"); guide.mark("heatmaps", "seen", { source: "demo" }); });
$("fgd-webhook").addEventListener("click", () => { guide.select("webhooks"); guide.mark("webhooks", "owned", { source: "demo" }); });
$("fgd-replay").addEventListener("click", () => { guide.select("session-replay"); guide.mark("session-replay", "owned", { source: "demo" }); });
$("fgd-reset").addEventListener("click", () => { guide.state = initial; guide.filter = "all"; guide.select("webhooks"); sync(); say("Progress reset."); });
$("fgd-panel").addEventListener("change", (e) => { guide.detail = e.target.checked ? "side" : "none"; });
sync();
</script>
</div>Referencia cultural
Pokémon Rojo y Verde, Game Freak / Nintendo (1996, videojuego). La enciclopedia del juego lista cada criatura por número en tres estados, desconocida (solo un número), vista (un vistazo y un nombre, sin datos) y capturada (la entrada completa), y los huecos de la lista numerada son lo que impulsa a los jugadores a completarla. En la interfaz, cualquier colección (funciones usadas, insignias conseguidas, lecciones completadas) se convierte en una cuadrícula numerada de huecos desconocidos, siluetas con una pista sobre cómo conseguirlas y tarjetas completas, con los huecos a la vista y cada subida de nivel revelada en su sitio.
API
Attributes
| Name | Tipo | Default | Description |
|---|---|---|---|
label | string | Collection | Name of the collection: shown above the score and used as the accessible name of the grid and the progress bar. |
owned-label / seen-label / unknown-label | string | Owned / Seen / Missing | Domain words for the three states (e.g. Used / Seen / Missing for features, Completed / Unlocked / Locked for lessons, Earned for badges). Used in the filters, the score (“12 of 30 used”), the detail panel and every accessible name (“seen, not yet used”). Past participles read best. |
filter | all | owned | seen | unknown | all | Current filter (reflected; the filter pills set it). An entry that changes state under a filter stays visible until the filter changes, so nothing jumps away mid-interaction. |
selected | string (entry id) | Active entry shown in the detail panel (reflected as the user moves). Defaults to the first entry. | |
total | number | Size of the series. Missing numbers up to it (and any gap between numbered entries) are rendered as nameless unknown slots, so secret entries never reach the DOM. | |
detail | side | below | none | side | Detail panel beside the grid (stacks below it when the element is narrower than 40rem), always below, or hidden. |
density | compact | Smaller tiles and artwork, for long series or narrow columns. | |
silhouette | shape | blur | shape | How seen images are hidden: shape uses the image alpha as a one-tone mask (icons, transparent PNG/SVG); blur shows a grayscale, blurred, low-contrast version (photos). SVG icons are always flattened into a solid shape. |
filters | "true" | "false" | true | "false" hides the filter pills (score and bar stay). |
persist | string | localStorage key: progress is saved on every change and restored on load. Stored states only raise what the markup says (knowledge never goes backward); reset() clears it. | |
data-entry (on children) | string (id) | Declares an entry in markup. Other fields: data-number, data-state (unknown | seen | owned), data-name, data-hint, data-summary, data-description, data-href, data-cta (link label while not owned; owned entries link as “Open {name}”), data-icon (#sprite-id or image URL), data-image. A child <svg> or <img> is the artwork; a child [data-details] is rich owned-only content moved into the panel. |
Properties
| Name | Tipo | Description |
|---|---|---|
entries | Array<{ id, number?, name, state?, hint?, summary?, description?, href?, cta?, icon?: Element | "#sprite-id" | url, image?, details?: Element | Array<{ label, value }> }> | Entries as data (replaces markup). Unnumbered entries take the lowest free numbers; an entry without a name can only be unknown. Can be set before the element is defined. |
state | Record<id, "unknown" | "seen" | "owned"> | Knowledge map. Setting it (e.g. from your server) applies silently: no events, no animation. |
counts | { owned, seen, unknown, known, total } | Read-only counts; known = owned + seen. |
strings | Partial<Record<string, string>> | Overrides for every visible text and announcement (keys: label, all, owned, seen, unknown, number, score, percent, progress, filters, details, nameOwned, nameSeen, nameUnknown, unknownTitle, unknownText, locked, howTo, open ({name}), learn, empty, announceDiscover, announceOwn, announceLost, announceFilter). English defaults. |
Methods
| Name | Description |
|---|---|
mark(id, state, { animate?, announce?, source? }) | Moves an entry (id, or number) to a state. Going up emits the cancelable mv-discover or mv-own first; going down is silent except for mv-change. Returns true when applied, false if vetoed, unknown id or a nameless slot. |
discover(id) / own(id) | Shorthands for mark(id, "seen") and mark(id, "owned"). |
select(id, { focus? }) | Makes an entry active in the panel (clears a filter that hides it). |
reset() | Clears persisted progress and returns every entry to its initial state, silently. |
Events
| Name | Description |
|---|---|
mv-discover | Cancelable, before an entry goes from unknown to seen. detail: { id, number, name, from, to, source, entry }. preventDefault() keeps it hidden (e.g. until your server confirms). |
mv-own | Cancelable, before an entry becomes owned (from seen, or straight from unknown with from: "unknown"). Same detail as mv-discover. |
mv-change | After any state change, up or down. detail: { id, from, to, source, counts }. Sync your backend here. |
mv-select | The user moved to another entry (click, arrows, typeahead, Tab). detail: { id, number, state, entry }. |
mv-activate | Enter, Space, double click or the panel's call to action on an entry. detail: { id, number, state, source: "keyboard" | "pointer" | "link", entry }. |
Content structure
| Name | Description |
|---|---|
[data-entry] children | Entries declared in markup (read once, then replaced by the rendered grid). Their [data-details] child is moved, not cloned, so its listeners survive. |
CSS classes
| Name | Description |
|---|---|
mv-field-guide-head | Summary: -title, -scoreline (-score with -count, -pct), -bar (role=progressbar, .mv-field-guide-seg[data-state]), -filters (.mv-field-guide-filter[aria-pressed] with -swatch, -filter-label, -filter-count). |
mv-field-guide-grid / mv-field-guide-entry | The listbox and its options; data-state="unknown | seen | owned", data-flash="discover | own" for a moment after leveling up. Parts: -num, -mark, -art, -name, -note. |
mv-field-guide-art | Artwork box with data-state and three stacked layers: -ghost (big number for unknown), -sil (silhouette), -full (full artwork, revealed by clip-path). |
mv-field-guide-panel | Detail panel (data-state): -panel-head, -panel-num (“Entry 14”), -panel-name, -status, -panel-body (-lead, -text, -facts, -locked, -howto, -cta). |
CSS variables
| Name | Default | Description |
|---|---|---|
--mv-field-guide-owned | var(--mv-accent) | Owned color: bar segment, swatch, check mark, selection ring. |
--mv-field-guide-owned-bg / --mv-field-guide-owned-ink | accent tint / var(--mv-accent-fg) | Background and icon color of owned artwork. |
--mv-field-guide-seen | accent mixed with --mv-bg-emphasis | Seen color (hatched bar segment, half swatch, discovery pulse). |
--mv-field-guide-silhouette | mix of --mv-fg and --mv-bg-muted | Tone of silhouettes. |
--mv-field-guide-min | 8.5rem (compact 6.75rem) | Minimum tile width (auto-fill columns). |
--mv-field-guide-art-size | 3.25rem (compact 2.5rem) | Artwork size in tiles. |
--mv-field-guide-panel-width | 18rem | Width of the side panel. |
--mv-field-guide-gap | var(--mv-space-2) | Gap between tiles. |
Accessibility
The grid is a real single-select listbox (aria-labelledby the collection title) whose options use a roving tabindex: Tab enters on the active entry, arrows move left/right and up/down by visual row (columns are measured, RTL mirrors left and right), Home/End jump to the ends, PageUp/PageDown move three rows, typing letters jumps to a known entry by name, Enter/Space/double click emit mv-activate; selection follows focus and drives the detail panel. Each option's accessible name carries its name and state in words (“Webhooks, seen, not yet used”; its place in the series comes from the listbox position), while unknown entries are named by their number only (“Entry 21, not discovered yet”) and never expose their name, and its hint or summary is linked with aria-describedby. State is never conveyed by the silhouette or color alone: tiles differ by border style (dashed slot), mark icon (check / eye) and text, swatches and bar segments differ by shape (full, half, hollow; hatched segment), and the panel shows a status pill with icon and label. The progress bar is a role=progressbar with an aria-valuetext of all three counts; filter pills are toggle buttons (aria-pressed) with their count in their name. Level-ups, filter changes and downgrades are announced once in a polite live region (“Discovered Heatmaps. 17 of 24 discovered.”, “Webhooks is now used. 11 of 24 used.”); focus is never moved by state changes, and an entry that changes state under a filter stays in place until the filter changes. Reduced motion (OS or data-motion="reduce"): the blur-and-fill reveal becomes a 200 ms crossfade and the pulses are dropped. Forced colors: selection uses Highlight, silhouettes GrayText, and the bar and swatches keep system-colored shapes.