익스클루시브

Door Dial <mv-door-dial>

페이지를 다시 빌드하지 않고 같은 링크, 액션, 코드 예제를 다른 컨텍스트(환경, 리전, 테넌트, 문서 버전, API 기본 주소)로 바꿔 주는 컨텍스트 다이얼입니다.

문화적 레퍼런스

하울의 움직이는 성, 다이애나 윈 존스(소설), 미야자키 하야오(영화) (1986, 책). 이 성에는 현관문이 하나뿐이지만, 열 때마다 다른 마을로 이어지도록 설정할 수 있습니다. UI에서는 그 메커니즘만 평범한 세그먼트 컨트롤로 가져왔습니다. 컨텍스트를 고르면 같은 링크, 동작, 코드 샘플이 다른 환경, 리전, 테넌트 또는 문서 버전을 가리키도록 바뀌고, 링크에 호버하면 그 링크가 이제 어느 마을로 열리는지 보여 줍니다.

작동 방식

페이지를 다시 빌드하지 않고도 같은 링크, 동작, 코드 샘플이 다른 컨텍스트(환경, 리전, 테넌트, 문서 버전, API 베이스)를 가리키도록 바꾸는 컨텍스트 다이얼입니다. 문은 <mv-door> 자식으로 선언하며, 각각 색상, 아이콘, 라벨로 구분됩니다. 다이얼은 작은 세그먼트 라디오 그룹이며, 색이 입혀진 표시기가 열린 문 아래로 미끄러집니다. href는 기본 문에 대해 한 번만 작성하고, 다른 모든 문은 선언적 규칙으로 이를 파생합니다. 베이스 교체(오리진 또는 경로 접두사, 또는 다이얼의 {door} 템플릿), 서브도메인을 유지하는 호스트 교체(api.example.com → api.staging.example.com), 쿼리 파라미터, 링크별 data-door-template 또는 data-door-href-<door>, 또는 JS 재작성 훅을 쓸 수 있습니다. 범위(for 선택자) 안의 링크, 폼 action, iframe을 다루며, data-door로 표시한 것 또는 links="matching"일 때 규칙이 적용되는 모든 링크가 대상입니다. 코드 샘플 안의 호스트도 재작성하고(data-door-text), 특정 문에서만 보이는 콘텐츠를 표시하며(data-door-only / data-door-not), 문 라벨을 채웁니다(data-door-label). 재작성된 링크에 호버하거나 포커스하면 문틀 모양의 미리보기가 열려 새 목적지를 보여 주고, 바뀐 부분은 강조하며 원래 값은 취소선으로 표시합니다. 선택은 저장(탭 간 동기화)하고 URL 파라미터에 반영할 수 있으며, 나중에 추가된 링크도 반영되고, 모든 전환은 취소 가능한 mv-door-change를 거치므로 앱이 프로덕션을 열기 전에 확인을 요청할 수 있습니다. resolve()는 JS 요청에도 같은 목적지를 제공합니다.

카테고리내비게이션
유형Web Component (<mv-door-dial>)
상태안정
키트파워 유저를 위한 내비게이션
Keywordsexclusive, culture, context-switcher, environment-switcher, region-picker, tenant-switcher, workspace-switcher, version-switcher, docs-version, api-base, link-rewrite, href, multi-region, staging, production, dev-portal, radiogroup, dial

When to use

  • Staff jump between production, staging and preview deploys in one console and every shortcut must open in the chosen environment
  • A multi-region admin or API reference must point its links and code samples at the US, EU or APAC endpoint the user picked
  • A docs site keeps several versions and switching version should re-target every doc link in place, not reload a different page
  • Support agents open the same tools for different tenants or workspaces and links must carry the selected tenant

Avoid when

  • The page only needs to show which environment it is running in, with no links to re-target → use Env Marker instead
  • The choice switches a local view or mode and no link or action depends on it → use Segmented instead
  • Each context is a different page with its own content: route to it instead of rewriting links, so URLs stay canonical and indexable for SEO

설치

node scripts/add.mjs door-dial --out ./src/marvelous

Marvelous UI MCP 서버를 사용하는 AI 에이전트: install_components({ slugs: ["door-dial"], target_dir: "<absolute path>/src/marvelous", framework: "react" }).

복사되는 파일(의존성 포함): tokens/tokens.css, core/base.css, core/dom.js, core/element.js, core/focus.js, core/motion.js, core/observe.js, core/position.js, components/door-dial/door-dial.js, components/door-dial/door-dial.css.

사용법

빠른 시작, 동작하는 가장 작은 마크업:

<mv-door-dial label="Environment">
  <mv-door value="production" label="Production" host="app.example.com" default></mv-door>
  <mv-door value="staging" label="Staging" host="staging.example.com"></mv-door>
</mv-door-dial>
<a href="https://app.example.com/settings" data-door>Open settings</a>

기본 마크업입니다. 여기서 시작해 속성, data-*, CSS 변수로 커스터마이즈하세요:

<div id="dd-demo" style="width:min(100%,60rem);margin-inline:auto">
  <style>
    #dd-demo { display:grid; gap:1.25rem; align-content:start }
    #dd-demo .dd-app { border:1px solid var(--mv-border); border-radius:var(--mv-radius-xl); background:var(--mv-surface); box-shadow:var(--mv-shadow-sm); overflow:hidden }
    #dd-demo .dd-bar { display:flex; align-items:center; gap:.75rem 1rem; flex-wrap:wrap; min-height:3.5rem; padding:.625rem 1rem .625rem 1.125rem; border-bottom:1px solid var(--mv-border) }
    #dd-demo .dd-brand { display:flex; align-items:center; gap:.5rem; font-weight:650; font-size:.9375rem; letter-spacing:-.01em; margin-inline-end:auto }
    #dd-demo .dd-logo { display:grid; place-items:center; width:1.75rem; height:1.75rem; border-radius:var(--mv-radius-md); background:var(--mv-fg); color:var(--mv-bg) }
    #dd-demo .dd-logo svg { width:1rem; height:1rem }
    #dd-demo .dd-brand small { color:var(--mv-fg-muted); font-weight:500; font-size:.8125rem }
    #dd-demo .dd-body { display:grid; grid-template-columns:minmax(0,1fr); gap:1rem; padding:1.125rem }
    #dd-demo .dd-status { display:flex; align-items:center; justify-content:space-between; gap:.5rem 1rem; flex-wrap:wrap; font-size:.8125rem; color:var(--mv-fg-muted) }
    #dd-demo .dd-status b { color:var(--mv-fg); font-weight:600 }
    #dd-demo .dd-note { display:flex; align-items:center; gap:.5rem; margin:0; padding:.5rem .75rem; border-radius:var(--mv-radius-md); font-size:.8125rem; background:color-mix(in oklab, var(--mv-door-color) 10%, var(--mv-surface)); border:1px solid color-mix(in oklab, var(--mv-door-color) 28%, transparent); color:var(--mv-fg) }
    #dd-demo .dd-note svg { flex:none; width:1rem; height:1rem; color:color-mix(in oklab, var(--mv-door-color) 75%, var(--mv-fg)) }
    #dd-demo .dd-grid { display:grid; grid-template-columns:repeat(auto-fit,minmax(min(100%,10rem),1fr)); gap:.625rem }
    #dd-demo .dd-card { display:grid; grid-template-columns:auto minmax(0,1fr); gap:.125rem .625rem; align-items:start; padding:.75rem .875rem; border:1px solid var(--mv-border); border-radius:var(--mv-radius-lg); background:var(--mv-bg-subtle); color:inherit; text-decoration:none; transition:border-color var(--mv-duration-fast), background-color var(--mv-duration-fast) }
    #dd-demo .dd-card:hover { border-color:var(--mv-border-strong); background:var(--mv-surface) }
    #dd-demo .dd-card:focus-visible { outline:none; box-shadow:var(--mv-focus-ring) }
    #dd-demo .dd-ico { grid-row:span 2; display:grid; place-items:center; width:2rem; height:2rem; border-radius:var(--mv-radius-md); background:var(--mv-bg-muted); color:var(--mv-fg-muted) }
    #dd-demo .dd-ico svg { width:1rem; height:1rem }
    #dd-demo .dd-card b { font-size:.8125rem; font-weight:600 }
    #dd-demo .dd-card span.dd-d { color:var(--mv-fg-muted); font-size:.75rem; line-height:1.4 }
    #dd-demo .dd-card[data-door-rewritten] b::after { content:""; display:inline-block; width:.375rem; height:.375rem; margin-inline-start:.375rem; border-radius:50%; background:var(--mv-door-color); vertical-align:middle }
    #dd-demo .dd-code { margin:0; padding:.75rem .875rem; border-radius:var(--mv-radius-lg); background:var(--mv-bg-muted); color:var(--mv-fg); font:.75rem/1.6 var(--mv-font-mono); overflow-x:auto; white-space:pre }
    #dd-demo .dd-code-head { display:flex; justify-content:space-between; align-items:center; margin:0 0 .375rem; font-size:.75rem; color:var(--mv-fg-muted) }
    #dd-demo .dd-foot { display:flex; align-items:center; justify-content:space-between; gap:.75rem 1.25rem; flex-wrap:wrap; padding:.75rem 1.125rem; border-top:1px solid var(--mv-border); background:var(--mv-bg-subtle) }
    #dd-demo .dd-switches { display:flex; gap:.5rem 1.25rem; flex-wrap:wrap }
    #dd-demo .mv-choice { font-size:.8125rem }
    #dd-demo .dd-log { margin:0; color:var(--mv-fg-subtle); font:.75rem/1.4 var(--mv-font-mono) }
    #dd-demo .dd-confirm { display:flex; align-items:center; gap:.75rem; flex-wrap:wrap; padding:.625rem 1.125rem; border-bottom:1px solid color-mix(in oklab, var(--mv-danger) 30%, transparent); background:color-mix(in oklab, var(--mv-danger) 8%, var(--mv-surface)); font-size:.8125rem }
    #dd-demo .dd-confirm[hidden] { display:none }
    #dd-demo .dd-confirm p { margin:0; flex:1 1 18rem }
    #dd-demo .dd-confirm span { display:flex; gap:.5rem }

    #dd-demo .dd-docs { display:grid; grid-template-columns:12rem minmax(0,1fr) }
    #dd-demo .dd-side { display:grid; align-content:start; gap:.125rem; padding:1rem .75rem; border-inline-end:1px solid var(--mv-border); background:var(--mv-bg-subtle) }
    #dd-demo .dd-side h5 { margin:.5rem .5rem .25rem; color:var(--mv-fg-subtle); font-size:.6875rem; font-weight:600; letter-spacing:.05em; text-transform:uppercase }
    #dd-demo .dd-side h5:first-child { margin-top:0 }
    #dd-demo .dd-side a { display:block; padding:.3125rem .5rem; border-radius:var(--mv-radius-md); color:var(--mv-fg-muted); font-size:.8125rem; text-decoration:none }
    #dd-demo .dd-side a:hover { background:var(--mv-bg-muted); color:var(--mv-fg) }
    #dd-demo .dd-side a[aria-current] { background:var(--mv-bg-muted); color:var(--mv-fg); font-weight:500 }
    #dd-demo .dd-side a:focus-visible { outline:none; box-shadow:var(--mv-focus-ring) }
    #dd-demo .dd-article { display:grid; gap:.75rem; align-content:start; padding:1.125rem 1.25rem 1.25rem }
    #dd-demo .dd-article h4 { margin:0; font-size:1.125rem; letter-spacing:-.01em }
    #dd-demo .dd-article p { margin:0; color:var(--mv-fg-muted); font-size:.875rem; line-height:1.6 }
    #dd-demo .dd-article a { color:var(--mv-fg); text-decoration:underline; text-decoration-color:var(--mv-border-strong); text-underline-offset:2px }
    #dd-demo .dd-article a:hover { text-decoration-color:currentColor }
    #dd-demo .dd-crumbs { color:var(--mv-fg-subtle); font-size:.75rem }
    @media (max-width:40rem) {
      #dd-demo .dd-docs { grid-template-columns:minmax(0,1fr) }
      #dd-demo .dd-side { border-inline-end:0; border-bottom:1px solid var(--mv-border) }
    }
  </style>

  <!-- 1. An operations console: one set of links, four environments (host swap, subdomains kept) -->
  <section class="dd-app" aria-label="Operations console">
    <header class="dd-bar">
      <span class="dd-brand">
        <span class="dd-logo" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3 3 8l9 5 9-5z"/><path d="m3 13 9 5 9-5"/></svg></span>
        Acme <small>Operations</small>
      </span>
      <mv-door-dial id="dd-env" label="Environment" for="#dd-console" value="staging">
        <mv-door value="production" label="Production" icon="shield" color="var(--mv-danger)" host="example.com" description="Live customer data" default></mv-door>
        <mv-door value="staging" label="Staging" icon="flask" color="var(--mv-warning)" host="staging.example.com" description="Mirrors production every night"></mv-door>
        <mv-door value="sandbox" label="Sandbox" icon="code" color="var(--mv-info)" host="sandbox.example.com" description="Test cards, simulated payouts"></mv-door>
        <mv-door value="preview" label="PR #482" icon="branch" color="var(--mv-accent)" host="pr-482.preview.example.com" description="Deploy preview · checkout-v2"></mv-door>
      </mv-door-dial>
    </header>
    <div class="dd-confirm" id="dd-confirm" role="group" aria-label="Confirm environment" hidden>
      <p id="dd-confirm-text"><b>Open Production?</b> Links on this page will act on live customer data.</p>
      <span>
        <button type="button" class="mv-button" data-variant="ghost" data-size="sm" id="dd-cancel">Stay</button>
        <button type="button" class="mv-button" data-variant="destructive" data-size="sm" id="dd-go" aria-describedby="dd-confirm-text">Open Production</button>
      </span>
    </div>

    <div class="dd-body" id="dd-console">
      <div class="dd-status">
        <span>Every link below opens in <b data-door-label>Production</b></span>
        <span class="mv-badge" data-variant="danger" data-door-only="production">Live data</span>
        <span class="mv-badge" data-variant="secondary" data-door-not="production">Test data</span>
      </div>

      <nav class="dd-grid" aria-label="Console shortcuts" data-door>
        <a class="dd-card" href="https://console.example.com/dashboard"><span class="dd-ico"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="3" width="7" height="9" rx="1.5"/><rect x="14" y="3" width="7" height="5" rx="1.5"/><rect x="14" y="12" width="7" height="9" rx="1.5"/><rect x="3" y="16" width="7" height="5" rx="1.5"/></svg></span><b>Dashboard</b><span class="dd-d">Orders, revenue</span></a>
        <a class="dd-card" href="https://logs.example.com/search?service=checkout&amp;level=error"><span class="dd-ico"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 6h16M4 12h10M4 18h13"/></svg></span><b>Error logs</b><span class="dd-d">Checkout · 24 h</span></a>
        <a class="dd-card" href="https://console.example.com/flags/checkout-v2"><span class="dd-ico"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 21V4M5 4h11l-2 4 2 4H5"/></svg></span><b>Feature flags</b><span class="dd-d">checkout-v2 · 25%</span></a>
        <a class="dd-card" href="https://console.example.com/developers/webhooks"><span class="dd-ico"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M18 16.98h-5.99c-1.1 0-1.95.94-2.48 1.9A4 4 0 0 1 2 17c.01-.7.2-1.4.57-2"/><path d="m6 17 3.13-5.78c.53-.97.1-2.18-.5-3.1a4 4 0 1 1 6.89-4.06"/><path d="m12 6 3.13 5.73C15.66 12.7 16.9 13 18 13a4 4 0 0 1 0 8"/></svg></span><b>Webhooks</b><span class="dd-d">6 endpoints</span></a>
        <a class="dd-card" href="https://status.example.net" data-door="off"><span class="dd-ico"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 12h4l3-8 4 16 3-8h4"/></svg></span><b>Public status</b><span class="dd-d">Same everywhere</span></a>
      </nav>

      <div>
        <p class="dd-code-head"><span>List recent orders</span><span data-door-label="description">Live customer data</span></p>
        <pre class="dd-code" data-door-text>curl https://api.example.com/v1/orders?limit=20 \
  -H "Authorization: Bearer &lt;your-api-key&gt;"</pre>
      </div>

      <p class="dd-note" data-door-only="production"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 3 2 20h20z"/><path d="M12 10v4M12 17h.01"/></svg>You are in Production: every change applies to live customers immediately.</p>
      <p class="dd-note" data-door-not="production"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="9"/><path d="M12 8v4l2.5 1.5"/></svg>Test environment: data resets every Sunday at 00:00 UTC.</p>
    </div>

    <div class="dd-foot">
      <span class="dd-switches">
        <label class="mv-choice"><input type="checkbox" role="switch" class="mv-switch" id="dd-ask" checked> Ask before opening Production</label>
        <label class="mv-choice"><input type="checkbox" role="switch" class="mv-switch" id="dd-remember"> Remember my choice</label>
      </span>
      <p class="dd-log" id="dd-log">Hover a shortcut to see where it now leads.</p>
    </div>
  </section>

  <!-- 2. A docs site: versions as a path template, every matching link re-targeted (compact dial) -->
  <section class="dd-app" aria-label="Developer documentation">
    <header class="dd-bar">
      <span class="dd-brand">
        <span class="dd-logo" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 19.5V5a2 2 0 0 1 2-2h14v16H6a2 2 0 0 0-2 2 2 2 0 0 0 2 2h14"/></svg></span>
        Acme <small>Docs</small>
      </span>
      <mv-door-dial id="dd-ver" label="Version" for="#dd-docs" links="matching" template="/docs/{door}" value="v2" compact>
        <mv-door value="v3" label="v3.2 (latest)" short="v3.2" icon="tag" color="var(--mv-success)" description="Current release" default></mv-door>
        <mv-door value="v2" label="v2.8" short="v2.8" icon="tag" color="var(--mv-warning)" description="Supported until Mar 31, 2027"></mv-door>
        <mv-door value="v1" label="v1 (end of life)" short="v1" icon="tag" color="var(--mv-fg-muted)" disabled></mv-door>
      </mv-door-dial>
    </header>
    <div class="dd-docs" id="dd-docs">
      <nav class="dd-side" aria-label="Documentation">
        <h5>Get started</h5>
        <a href="/docs/v3/quickstart">Quickstart</a>
        <a href="/docs/v3/authentication">Authentication</a>
        <h5>Guides</h5>
        <a href="/docs/v3/webhooks" aria-current="page">Webhooks</a>
        <a href="/docs/v3/errors">Errors and retries</a>
        <a href="/docs/v3/pagination">Pagination</a>
        <h5>More</h5>
        <a href="/changelog">Changelog</a>
        <a href="https://code.example.net/acme/sdk-node">Node.js SDK</a>
      </nav>
      <article class="dd-article">
        <span class="dd-crumbs">Guides / Webhooks · <span data-door-label>v3.2 (latest)</span></span>
        <h4>Receiving webhooks</h4>
        <p class="dd-note" data-door-only="v2"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="9"/><path d="M12 8v4M12 16h.01"/></svg>You are reading the v2 docs. v2 is supported until March 31, 2027.</p>
        <p>Acme sends an HTTPS <code>POST</code> to your endpoint for every event. Verify each request with the <a href="/docs/v3/webhooks#signatures">signing secret</a> before trusting it, and answer within 10 seconds or the delivery is <a href="/docs/v3/errors#retries">retried with backoff</a>.</p>
        <p>New in this release? Read the <a href="/changelog">changelog</a> or jump back to the <a href="/docs/v3/quickstart">quickstart</a>.</p>
      </article>
    </div>
  </section>

  <script type="module">
    const env = document.getElementById("dd-env");
    const ver = document.getElementById("dd-ver");
    const log = document.getElementById("dd-log");
    const ask = document.getElementById("dd-ask");
    const remember = document.getElementById("dd-remember");
    const confirmBar = document.getElementById("dd-confirm");
    let confirmed = false;

    // Veto: opening Production needs an explicit confirmation.
    env.addEventListener("mv-door-change", (e) => {
      if (e.detail.value !== "production" || !ask.checked || confirmed) { confirmed = false; return; }
      e.preventDefault();
      confirmBar.hidden = false;
      document.getElementById("dd-go").focus();
      log.textContent = "Switch to Production is waiting for confirmation.";
    });
    document.getElementById("dd-go").addEventListener("click", () => {
      confirmed = true;
      confirmBar.hidden = true;
      env.turn("production", { source: "user" });
      env.focus();
    });
    document.getElementById("dd-cancel").addEventListener("click", () => {
      confirmBar.hidden = true;
      env.focus();
      log.textContent = `Stayed in ${env.door.label}.`;
    });
    env.addEventListener("mv-door-rewrite", (e) => {
      if (e.detail.source === "init") return;
      const n = e.detail.count;
      log.textContent = `${e.detail.door.label} · ${n} link${n === 1 ? "" : "s"} re-targeted, API sample updated.`;
    });
    remember.addEventListener("change", () => { env.persist = remember.checked ? "dd-demo-env" : null; });

    // Demo only: never leave the page, show where the click would have gone.
    document.getElementById("dd-demo").addEventListener("click", (e) => {
      const a = e.target.closest("a[href]");
      if (!a) return;
      e.preventDefault();
      if (a.closest("#dd-console")) log.textContent = `Would open ${a.href}`;
    });
  </script>
</div>

API

Attributes

Name유형DefaultDescription
valuestringValue of the open door (reflected). Initial priority: the query URL parameter, then the persisted choice, then value, then the default door. Setting it applies the door without mv-door-change.
labelstringOpen links in (visually hidden)Visible caption before the doors (“Environment”, “Region”, “Version”) and accessible name of the radio group. Without it, the group is named by the hidden default.
forCSS selectorScope roots whose content is re-targeted (every match, including ones added later). Without it: the nearest [data-door-scope] ancestor, else the whole document body.
linksmarked | matchingmarkedmarked: only elements carrying data-door (or inside a container that does). matching: also every a[href], area[href], form[action] and iframe[src] of the scope that some door’s rules would change; data-door="off" still opts out.
templatestring with {door}Base shared by every door without its own base, e.g. "/docs/{door}" or "https://{door}.api.example.com": the default door’s filled base is swapped for the open door’s.
persiststringOpt-in: saves the chosen door in localStorage under mv-door-dial:<key>, restores it on load and follows changes made in other tabs (storage event).
querystringOpt-in: URL parameter mirroring the door (history.replaceState, removed on the default door) so a shared page link opens the same door.
previewauto | always | offautoDoor-frame preview on hover (after 260 ms) and keyboard focus of a managed link: auto only when the destination differs from the authored href, always also on the default door.
compactbooleanDoors show their short text (or only their icon); the full label stays the accessible name and a native tooltip.
disabledbooleanLocks the dial on its current door.
data-current / data-defaultstring / booleanSet on the dial by the component: open door value, and whether it is the default door (styleable).
<mv-door> value, label, short, description, icon, colorstringsOne door. label (or text content) is the name; short replaces it in compact mode; description appears in the preview; icon is a built-in name (globe, server, laptop, flask, cloud, shield, book, code, flag, building, branch, tag, user, bolt) or an inline <svg> child; color is any CSS color (default: --mv-door-dial-color-1…6 by position).
<mv-door> default, disabledbooleandefault: the door hrefs are authored for (else the first door). disabled: shown but cannot be opened (end-of-life version, region not provisioned).
<mv-door> base, host, paramstringsRewrite rules, applied in this order. base: URL or path prefix swapped for the default door’s on a path boundary ("https://api.eu.example.com", "/docs/v2"). host: host swap for any URL on the default door’s host (else its absolute base’s host, else the page’s) or one of its subdomains; accepts a scheme and port ("http://localhost:3000"); URLs already on another door’s host are left alone. param: query params set on matched or same-origin URLs ("region=eu&tenant=acme"), the default door’s keys being removed first.
data-door"" | "on" | "off" | dial idOn a link, form or iframe: managed. On any other element: every URL element inside is managed. off opts out (nearest wins); a dial id binds content to one dial when several share a scope.
data-door-attrstringURL attribute to rewrite when it isn’t the tag default (href, action, src), e.g. formaction on a button or data-src.
data-door-href-<door> / data-door-templatestringPer-link overrides: an explicit destination for one door, or a template with {door} and {label} used for every door.
data-door-textbooleanText inside (code samples, API base labels) has the default door’s base and host swapped. Originals are kept and restored.
data-door-only / data-door-notdoor values (space or comma separated)Element shown only in those doors, or hidden in them (hidden attribute; the original state is restored when the dial goes away).
data-door-label"" | label | short | value | descriptionText replaced by the open door’s field.
data-door-rewritten / data-door-flash / data-door-currentset by the componentOn managed elements whose destination differs from the authored one; on elements that just changed (1.2 s outline); on scope roots (open door value). --mv-door-color is set on all of them.

Properties

Name유형Description
valuestringOpen door value; setting it applies silently (no mv-door-change).
door / defaultDoor / doorsDoor | Door[]Read-only snapshots: { value, label, short, description, color, icon, base, host, param, default, disabled, element }.
rewrite(url: URL, door, { href, element, defaultDoor }) => string | URL | nullCustom rule run after the declarative ones for non-default doors (tenant ids in paths, signed URLs…); return null to keep the result. Setting it re-applies.
stringsPartial<Record<string, string>>Overrides for visible texts and announcements (keys: label, announce ({label}), links ({count}), link, hint ({label}), previewTitle, previewInstead). English defaults.
for / links / template / persist / query / preview / compact / disabled / labelreflectedMirror the attributes.

Methods

NameDescription
turn(value, { source? })Opens a door like a user would: emits the cancelable mv-door-change, then applies it. Returns false if vetoed, unknown or disabled.
next() / previous()Turns to the next or previous enabled door (wraps).
resolve(href, value?, element?)Where an href authored for the default door leads in a door (default: the open one). Use it for fetch() calls and anything built in JS.
refresh()Re-scans the scope and re-applies (links added later are already picked up by a MutationObserver).
focus()Focuses the open door’s radio.

Events

NameDescription
mv-door-changeCancelable, before a door opens. detail: { value, previous, door, previousDoor, source: "user" | "api" | "storage" }. preventDefault() keeps the current door (e.g. to confirm before production).
mv-door-rewriteAfter a door was applied, including on first connection. detail: { value, door, count (URLs that changed), source: "init" | "user" | "api" | "storage" }.

Content structure

NameDescription
<mv-door> childrenThe doors, in order. They are config only (display: none) and watched: adding, removing or editing one updates the dial.

CSS classes

NameDescription
mv-door-dial-labelVisible caption, labels the radio group.
mv-door-dial-track / mv-door-dial-doorThe radiogroup and its role="radio" buttons (data-selected, data-default, --_c = door color): -glyph (icon, or data-icon="dot"), -name.
mv-door-dial-indicatorTinted surface with a threshold line, sliding under the open door.
mv-door-dial-previewDoor-frame preview (popover, aria-hidden): -head, -glyph, -title, -desc, -url (mark = changed part), -was / -old (s = replaced part).

CSS variables

NameDefaultDescription
--mv-door-color(open door color)Set by the component on the dial, scope roots and managed elements: tint anything by the open door.
--mv-door-dial-color-1 … -6accent, success, warning, info, danger, fg-mutedPalette for doors without a color, by position (defined on :root).
--mv-door-dial-bgvar(--mv-surface)Dial background.
--mv-door-dial-radiusvar(--mv-radius-lg)Dial corner radius (doors follow it).
--mv-door-dial-heightvar(--mv-control-sm)Height of a door.

Accessibility

The doors are a real radio group (role="radiogroup" named by the visible caption, role="radio" buttons with aria-checked): Tab reaches the open door, arrow keys move and open the next door (roving tabindex, wraps), Home and End jump to the first and last, disabled doors are skipped. Every door has an icon and a text label as well as its color, so nothing relies on color alone; in compact mode the full label stays the accessible name. A polite live region announces each turn with the number of changed URLs (“Now opening in: EU region. 5 links updated.”). Rewritten links keep their own accessible name: when they lead somewhere other than their authored door, aria-describedby points to a shared hidden hint (“Opens in: EU region”), removed again on the default door. The door-frame preview is aria-hidden with pointer-events: none (the hint carries the same information), opens on hover and on keyboard focus (focus-visible) of a rewritten link, and Escape or leaving the link closes it. A vetoed turn (mv-door-change prevented) puts focus back on the open door unless the app moved it, for example to a confirmation button. Door-only content uses the hidden attribute, so it leaves the accessibility tree too. Reduced motion (OS or data-motion="reduce"): the indicator jumps instead of sliding, the preview appears without motion and the outline flash on changed links is skipped. Forced colors: the open door and the indicator use Highlight, dots and the indicator line use CanvasText. Removing the dial restores every href, text, label, hidden state and aria-describedby it changed.

이 페이지는 AI로 번역되었습니다. 번역 문제 신고