익스클루시브
Field Guide <mv-field-guide>
모든 항목이 세 단계의 정보 수준을 가져서 완료한 것만큼 빠진 것도 잘 보이는 번호 매긴 컬렉션 그리드입니다. unknown(점선 테두리와 선택적 힌트가 있는 빈 번호 칸), seen(단색 실루엣으로 처리한 이미지, 이름, 잠긴 상세 정보, 획득 방법 힌트), owned(요약, 설명, 정보, 행동 유도 버튼이 있는 풀 컬러 카드)로 구성됩니다.
문화적 레퍼런스
포켓몬스터 레드·그린, Game Freak / Nintendo(1996년, 게임). 게임 속 도감은 모든 포켓몬을 번호순으로 세 가지 상태로 기록합니다: 미확인(번호만), 발견(잠깐 본 모습과 이름, 데이터 없음), 포획(완전한 항목). 그리고 번호 목록의 빈칸이 플레이어가 도감을 완성하도록 이끕니다. UI에서는 어떤 컬렉션이든(사용한 기능, 획득한 배지, 완료한 강의) 미확인 슬롯, 얻는 방법 힌트가 붙은 실루엣, 완전한 카드로 이루어진 번호 매긴 그리드가 되며, 빈칸은 보이는 채로 남고 각 레벨 업은 제자리에서 드러납니다.
작동 방식
모든 항목이 세 단계의 지식 수준을 가져 빠진 것도 완료한 것만큼 잘 보이는 번호 매긴 컬렉션 그리드: 미확인(점선 윤곽과 선택적 힌트가 있는 빈 번호 슬롯), 발견(단색 실루엣으로 처리된 아트워크, 이름, 잠긴 세부 정보, “how to get it” 힌트), 보유(요약, 설명, 정보, 행동 유도 버튼이 있는 풀 컬러 카드). 번호의 빈틈은 이름 없는 슬롯이 되므로 사용자는 시리즈의 구멍을 볼 수 있습니다. 레벨 업은 제자리에서 애니메이션됩니다: 발견 시 실루엣이 흐림 속에서 선명해지고, 보유 시 완전한 아트워크가 아래에서부터 채워지며 한 번의 펄스가 이어집니다(동작 줄이기 설정에서는 단순 크로스페이드). 요약 헤더는 세그먼트 막대(보유 / 발견 / 미확인, 색상뿐 아니라 모양으로도 구분)와 함께 “12 of 30 used”를 표시하고, 개수가 붙은 필터 알약 버튼을 제공합니다. 고정 상세 패널은 포커스된 항목을 따라갑니다. 그리드는 시각적 행 기준 2D 화살표 탐색, Home/End, PageUp/PageDown, 이름 타이프어헤드를 지원하는 실제 listbox입니다. mark(id, state)로 제어하며 취소 가능한 mv-discover와 mv-own을 발생시키고, 진행 상황은 localStorage에 유지할 수 있으며(새로 고침해도 지식은 절대 뒤로 가지 않음), 상태 레이블은 도메인에 맞게 바뀝니다(Used, Earned, Completed…).
| 카테고리 | 데이터 표시 |
|---|---|
| 유형 | Web Component (<mv-field-guide>) |
| 상태 | 안정 |
| 함께 설치되는 항목 | 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
설치
node scripts/add.mjs field-guide --out ./src/marvelousMarvelous UI MCP 서버를 사용하는 AI 에이전트: install_components({ slugs: ["field-guide"], target_dir: "<absolute path>/src/marvelous", framework: "react" }).
복사되는 파일(의존성 포함): tokens/tokens.css, core/base.css, components/button/button.css, core/dom.js, core/element.js, core/focus.js, components/field-guide/field-guide.js, components/field-guide/field-guide.css.
사용법
기본 마크업입니다. 여기서 시작해 속성, data-*, 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 Vantro <span class="mv-badge" data-variant="info" data-shape="pill">Acme 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="Vantro 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="off">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>API
Attributes
| Name | 유형 | 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 | 유형 | 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.