익스클루시브

As Of <mv-as-of>

과거 데이터를 보여 줄 수 있는 데이터 앱을 위한 시점(“as of”) 컨트롤로, 과거 수치를 현재 수치로 착각하는 일이 없도록 합니다.

문화적 레퍼런스

백 투 더 퓨처, 로버트 저메키스 (1985, 영화). 타임머신의 계기판은 항상 목적지 시간, 현재 시간, 마지막 출발 시간이라는 세 가지 표시를 보여 주므로, 여행자는 출발한 곳에 비해 지금 언제에 있는지 놓치지 않습니다. UI에서 데이터 앱은 같은 세 행(보고 있는 시점, 실시간 현재, 마지막 이동 전에 있던 시점으로 클릭 한 번이면 돌아감)을 보여 주며, 과거에 있을 때는 차분한 배너, 프레임으로 표시된 데이터, 잠긴 편집 기능을 제공합니다.

작동 방식

과거 데이터를 보여 줄 수 있는 데이터 앱을 위한 시점(“as of”) 컨트롤로, 과거 수치를 현재 수치로 착각하는 일이 없도록 합니다. 세 행이 항상 표시됩니다: Viewing(선택한 시점 또는 실시간 데이터. 날짜 또는 날짜 및 시간 필드, 월말을 유지하는 ±1 시간/일/주/월/분기/연 단위 이동, “End of last quarter” 같은 프리셋과 최근 목적지를 담은 Jump to 메뉴), Now(시간대가 표시되는 실시간 시계), Left from(마지막 이동 전에 있던 시점. 클릭 한 번으로 돌아갈 수 있어 두 날짜 비교가 토글이 됩니다). 과거를 보는 동안에는 차분한 배너(“You’re viewing data as of Jun 30, 2026. Return to now”)와 데이터 영역을 둘러싼 선택적 프레임이 이를 말로 알려 주며, 범위 안의 모든 [data-as-of-live-only] 컨트롤(편집, 조정, 저장 버튼)은 현재로 돌아올 때까지 비활성화(inert)됩니다. 각 이동은 취소 가능한 mv-travel을 거치며, 이 이벤트의 waitUntil(promise)은 데이터를 다시 불러오는 동안 대기 상태를 표시하고 불러오기에 실패하면 안내와 함께 롤백합니다. 빠르게 연속으로 이동하면 하나의 이동으로 합쳐지고, 미래는 실시간으로, min 이전은 가장 이른 사용 가능 데이터로 제한됩니다. 시점은 URL 매개변수에 담을 수 있으며(뒤로/앞으로 지원), 최근 목적지는 localStorage에 유지할 수 있습니다.

카테고리내비게이션
유형Web Component (<mv-as-of>)
상태안정
키트파워 유저를 위한 내비게이션
함께 설치되는 항목button, input, select
Keywordsexclusive, culture, point-in-time, as-of, time-travel, history, snapshot, audit, versioning, dashboard, report, financial, period-end, read-only, url-state, temporal

When to use

  • A dashboard or report can be recomputed as of a past date and viewers must never confuse it with current figures
  • An audit or history view shows a configuration, permissions or inventory exactly as it stood at a given moment
  • Financial statements are reviewed at period ends (month, quarter, year) with quick back-and-forth between two dates
  • Editing must be blocked while someone is looking at a historical snapshot, and resume as soon as they return to now

Avoid when

  • The user picks a date as form input (due date, booking, birthday), not a moment to view the whole app at → use Date Picker instead
  • The job is to display one moment or deadline clearly across time zones → use Dateline instead
  • The need is to undo or compare edits of a single form or document rather than view data at a past moment → use Rewind instead

설치

node scripts/add.mjs as-of --out ./src/marvelous

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

복사되는 파일(의존성 포함): tokens/tokens.css, core/base.css, components/button/button.css, components/input/input.css, components/select/select.css, core/dismiss.js, core/dom.js, core/element.js, core/focus.js, core/observe.js, core/position.js, components/as-of/as-of.js, components/as-of/as-of.css.

사용법

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

<mv-as-of frame="#report" min="2024-01-01"></mv-as-of>
<section id="report">…your figures…</section>

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

<div id="ao-demo" style="width:min(100%,66rem);margin-inline:auto">
  <style>
    #ao-demo { display:grid; grid-template-columns:minmax(0,1fr); gap:2.5rem; align-content:start }
    #ao-demo .ao-main { display:grid; grid-template-columns:minmax(0,1fr) 24rem; gap:1.5rem; align-items:start }
    #ao-demo .ao-report { display:grid; gap:1rem; padding:1.25rem; border:1px solid var(--mv-border); border-radius:var(--mv-radius-xl); background:var(--mv-surface); box-shadow:var(--mv-shadow-xs) }
    #ao-demo .ao-top { display:flex; align-items:flex-start; justify-content:space-between; gap:.75rem 1rem; flex-wrap:wrap }
    #ao-demo .ao-top h3 { margin:0; font-size:1.0625rem; letter-spacing:-.01em }
    #ao-demo .ao-top p { margin:.125rem 0 0; color:var(--mv-fg-muted); font-size:.8125rem }
    #ao-demo .ao-actions { display:flex; gap:.5rem; flex-wrap:wrap }
    #ao-demo .ao-kpis { display:grid; grid-template-columns:repeat(4,minmax(0,1fr)); gap:.75rem; margin:0 }
    #ao-demo .ao-kpi { display:grid; gap:.25rem; padding:.75rem .875rem; border:1px solid var(--mv-border); border-radius:var(--mv-radius-lg); background:var(--mv-bg-subtle) }
    #ao-demo .ao-kpi dt { color:var(--mv-fg-muted); font-size:.75rem }
    #ao-demo .ao-kpi dd { margin:0; font-size:1.25rem; font-weight:650; letter-spacing:-.02em; font-variant-numeric:tabular-nums }
    #ao-demo .ao-kpi small { display:block; color:var(--mv-fg-muted); font-size:.6875rem; font-weight:500; letter-spacing:0 }
    #ao-demo table { width:100%; border-collapse:collapse; font-size:.8125rem }
    #ao-demo caption { text-align:start; padding:0 0 .5rem; color:var(--mv-fg-muted); font-size:.75rem; font-weight:500 }
    #ao-demo th, #ao-demo td { padding:.5rem .625rem; border-bottom:1px solid var(--mv-border); text-align:start; white-space:nowrap }
    #ao-demo th { color:var(--mv-fg-muted); font-size:.6875rem; font-weight:600; letter-spacing:.04em; text-transform:uppercase }
    #ao-demo td.num, #ao-demo th.num { text-align:end; font-variant-numeric:tabular-nums }
    #ao-demo tr:last-child td { border-bottom:0 }
    #ao-demo .ao-side { display:grid; gap:.875rem }
    #ao-demo .ao-panel { display:grid; gap:.625rem; padding:.875rem 1rem; border:1px solid var(--mv-border); border-radius:var(--mv-radius-lg); background:var(--mv-bg-subtle); font-size:.8125rem }
    #ao-demo .ao-log { min-height:1.25rem; margin:0; color:var(--mv-fg-subtle); font:.75rem/1.45 var(--mv-font-mono); overflow-wrap:anywhere }
    #ao-demo .ao-h { margin:0 0 .75rem; font-size:.8125rem; font-weight:600; color:var(--mv-fg-muted) }
    #ao-demo .ao-flags { padding:1rem 1.25rem .75rem; border:1px solid var(--mv-border); border-radius:var(--mv-radius-xl); background:var(--mv-surface); margin-top:1.25rem }
    #ao-demo .ao-flag-name { font-family:var(--mv-font-mono); font-size:.75rem }
    @media (max-width:62rem) { #ao-demo .ao-main { grid-template-columns:minmax(0,1fr) } }
    @media (max-width:40rem) { #ao-demo .ao-kpis { grid-template-columns:repeat(2,minmax(0,1fr)) } #ao-demo .ao-hide-sm { display:none } #ao-demo td:first-child { white-space:normal } #ao-demo .ao-report, #ao-demo .ao-flags { padding-inline:.875rem } #ao-demo th, #ao-demo td { padding-inline:.375rem } }
  </style>

  <!-- 1. Inventory report with the stacked control beside it -->
  <div class="ao-main" id="ao-app">
    <section class="ao-report" id="ao-report" aria-labelledby="ao-report-title">
      <div class="ao-top">
        <div>
          <h3 id="ao-report-title">Inventory &amp; revenue</h3>
          <p>Harborline Supply · all warehouses</p>
        </div>
        <div class="ao-actions">
          <button type="button" class="mv-button" data-variant="outline" data-size="sm" data-as-of-live-only>Adjust stock</button>
          <button type="button" class="mv-button" data-size="sm" data-as-of-live-only>Edit forecast</button>
        </div>
      </div>
      <dl class="ao-kpis" id="ao-kpis"></dl>
      <table>
        <caption id="ao-caption">Top SKUs by value on hand</caption>
        <thead><tr><th scope="col">SKU</th><th scope="col" class="ao-hide-sm">Warehouse</th><th scope="col" class="num">On hand</th><th scope="col" class="num">Value</th><th scope="col">Status</th></tr></thead>
        <tbody id="ao-rows"></tbody>
      </table>
    </section>

    <div class="ao-side">
      <mv-as-of id="ao-main" param="asof" frame="#ao-report" scope="#ao-app" min="2024-01-01" storage-key="ao-demo-recent"></mv-as-of>
      <div class="ao-panel">
        <label class="mv-choice" data-control="end">
          <input type="checkbox" role="switch" class="mv-switch" id="ao-fail">
          <span class="mv-choice-text"><span class="mv-choice-title">Make the next fetch fail</span><span class="mv-choice-description">The jump is rolled back and announced</span></span>
        </label>
        <p class="ao-log" id="ao-log" aria-live="polite">Loading…</p>
      </div>
    </div>
  </div>

  <!-- 2. Bar layout, minute precision: a production config audited at a past moment -->
  <div>
    <p class="ao-h">Feature flags · production (bar layout, minute precision)</p>
    <div id="ao-flags-app">
      <mv-as-of id="ao-bar" layout="bar" precision="minute" steps="hour, day, week" unit="hour" presets="7d, 30d, last-month" frame="#ao-flags" scope="#ao-flags-app" label="Config point in time"></mv-as-of>
      <div class="ao-flags" id="ao-flags">
        <table>
          <caption id="ao-flag-caption">Flags</caption>
          <thead><tr><th scope="col">Flag</th><th scope="col">State</th><th scope="col" class="num">Rollout</th><th scope="col" class="ao-hide-sm">Changed by</th><th scope="col"><span class="mv-sr-only">Actions</span></th></tr></thead>
          <tbody id="ao-flag-rows"></tbody>
        </table>
      </div>
    </div>
  </div>

  <script type="module">
    const main = document.getElementById("ao-main");
    const bar = document.getElementById("ao-bar");
    const log = document.getElementById("ao-log");
    const fail = document.getElementById("ao-fail");
    const usd = new Intl.NumberFormat("en-US", { style: "currency", currency: "USD", maximumFractionDigits: 0 });
    const int = new Intl.NumberFormat("en-US");
    const short = new Intl.DateTimeFormat("en-US", { month: "short", day: "numeric", year: "numeric" });

    // Deterministic "database": the same moment always returns the same figures.
    const seed = (str) => { let h = 2166136261; for (const c of str) { h ^= c.charCodeAt(0); h = Math.imul(h, 16777619); } return () => ((h = Math.imul(h ^ (h >>> 15), 2246822507) ^ Math.imul(h ^ (h >>> 13), 3266489909)) >>> 0) / 4294967296; };
    const cell = (text, cls) => { const td = document.createElement("td"); if (cls) td.className = cls; td.append(text); return td; };
    const badge = (text, variant) => { const b = document.createElement("span"); b.className = "mv-badge"; b.dataset.variant = variant; b.dataset.size = "sm"; b.textContent = text; return b; };

    const SKUS = [
      ["Ceramic pour-over set", "Rotterdam"], ["Linen apron, charcoal", "Reno"], ["Cast-iron skillet 26 cm", "Rotterdam"],
      ["Oak cutting board", "Monterrey"], ["Stoneware mug, set of 4", "Reno"],
    ];
    function renderReport(date) {
      const key = date ? date.toISOString().slice(0, 10) : "live";
      const r = seed(key);
      const f = date ? 0.82 + r() * 0.16 : 1;
      const kpis = [
        ["Revenue, trailing 30 days", usd.format(Math.round(412_800 * f)), date ? `as of ${short.format(date)}` : "live"],
        ["Units on hand", int.format(Math.round(18_420 * (0.9 + r() * 0.2))), "5 warehouses"],
        ["Open orders", int.format(Math.round(236 * (0.7 + r() * 0.5))), "awaiting shipment"],
        ["Gross margin", `${(41.2 + (r() - 0.5) * 6).toFixed(1)}%`, "trailing 30 days"],
      ];
      document.getElementById("ao-kpis").replaceChildren(...kpis.map(([k, v, s]) => {
        const div = document.createElement("div"); div.className = "ao-kpi";
        const dt = document.createElement("dt"); dt.textContent = k;
        const dd = document.createElement("dd"); dd.textContent = v;
        const sm = document.createElement("small"); sm.textContent = s; dd.append(sm);
        div.append(dt, dd); return div;
      }));
      document.getElementById("ao-caption").textContent = date ? `Top SKUs by value on hand, as of ${short.format(date)}` : "Top SKUs by value on hand, live";
      document.getElementById("ao-rows").replaceChildren(...SKUS.map(([name, wh], i) => {
        const qty = Math.round((1400 - i * 210) * (0.25 + r() * 0.9));
        const value = qty * (18 + (5 - i) * 7);
        const status = qty < 320 ? ["Reorder", "danger"] : qty < 620 ? ["Low", "warning"] : ["In stock", "success"];
        const tr = document.createElement("tr");
        tr.append(cell(name), cell(wh, "ao-hide-sm"), cell(int.format(qty), "num"), cell(usd.format(value), "num"), cell(badge(...status)));
        return tr;
      }));
    }

    const FLAGS = ["checkout.one-page", "search.semantic-ranking", "billing.annual-discount", "mobile.dark-launch"];
    const PEOPLE = ["Aiko Tanaka", "Lucas Moreau", "Priya Raman", "Mateo Silva"];
    function renderFlags(date) {
      const key = date ? `${date.toISOString().slice(0, 13)}` : "live";
      const r = seed(key);
      document.getElementById("ao-flag-caption").textContent = date ? `Flags as of ${date.toLocaleString("en-US", { dateStyle: "medium", timeStyle: "short" })}` : "Flags, live";
      document.getElementById("ao-flag-rows").replaceChildren(...FLAGS.map((flag, i) => {
        const on = (i + Math.floor(r() * 3)) % 3 !== 0;
        const rollout = on ? [5, 25, 50, 100][Math.floor(r() * 4)] : 0;
        const tr = document.createElement("tr");
        const name = document.createElement("span"); name.className = "ao-flag-name"; name.textContent = flag;
        const edit = document.createElement("button");
        edit.type = "button"; edit.className = "mv-button"; edit.dataset.variant = "ghost"; edit.dataset.size = "sm";
        edit.dataset.asOfLiveOnly = ""; edit.textContent = "Edit"; edit.setAttribute("aria-label", `Edit ${flag}`);
        tr.append(cell(name), cell(badge(on ? "On" : "Off", on ? "success" : "secondary")), cell(`${rollout}%`, "num"), cell(PEOPLE[(i + Math.floor(r() * 4)) % 4], "ao-hide-sm"), cell(edit, "num"));
        return tr;
      }));
    }

    // A fake API: 450 ms round trip, optionally failing once.
    const fetchAsOf = (reason) => new Promise((resolve, reject) => {
      if (reason === "api") return resolve();
      setTimeout(() => {
        if (fail.checked) { fail.checked = false; reject(new Error("504 Gateway Timeout")); } else resolve();
      }, 450);
    });
    const label = (d) => (d ? short.format(d) : "live");

    main.addEventListener("mv-travel", (e) => {
      const { from, to, reason } = e.detail;
      log.textContent = `GET /reports/inventory?asof=${e.detail.toValue ?? "now"} · ${reason}`;
      e.detail.waitUntil(fetchAsOf(reason).then(() => renderReport(to)));
    });
    main.addEventListener("mv-arrive", (e) => { log.textContent = `mv-arrive · ${label(e.detail.from)} → ${label(e.detail.to)}`; });
    main.addEventListener("mv-travel-error", (e) => { log.textContent = `Fetch failed (${e.detail.error.message}) · rolled back to ${label(e.detail.from)}`; renderReport(e.detail.from); });

    bar.addEventListener("mv-travel", (e) => { e.detail.waitUntil(fetchAsOf(e.detail.reason).then(() => renderFlags(e.detail.to))); });

    // Start in the past so all three rows are filled: live → end of last month → end of last quarter.
    const now = new Date();
    const lastQuarter = new Date(now.getFullYear(), Math.floor(now.getMonth() / 3) * 3, 0);
    let first = new Date(now.getFullYear(), now.getMonth(), 0);
    if (+first === +lastQuarter) first = new Date(now.getFullYear(), now.getMonth() - 1, 0);
    renderReport(main.date);
    renderFlags(null);
    if (main.live) {
      main.travel(first).then(() => main.travel(lastQuarter));
    }
    const twoDaysAgo = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 2, 16, 45);
    bar.travel(twoDaysAgo);
  </script>
</div>

API

Attributes

Name유형DefaultDescription
valuestring ("2026-06-30" | "2026-06-30T17:30" | ISO with offset)The viewed moment as a local wall time; absent = live. Reflected after every travel. Setting it from outside applies silently (no mv-travel, Left from unchanged): call travel() to go through the event flow. Future moments resolve to live, earlier ones than min are clamped to min.
precisionday | minutedayday: a date field, and a value means the end of that day (close of business); picking today means live. minute: a date-time field and values to the minute; anything within the last minute means live.
mindate or date-timeEarliest moment with data (retention start, company founding). Stepping and typing clamp to it and the clamp is announced; the back step is disabled there.
stepscomma list of hour | day | week | month | quarter | yearday, week, monthUnits offered in the step-size select (hidden when only one). Month, quarter and year steps keep end of month sticky: Mar 31 → Apr 30 → May 31.
unitone of stepsday (or the first step)Initially selected step unit.
presetscomma list of yesterday | last-week | last-month | last-quarter | last-year | 7d | 30d | 90d | 1yyesterday, last-week, last-month, last-quarter, last-yearBuilt-in presets of the Jump to menu, resolved against the current date when the menu opens (weeks follow the locale's first day). The presets property also accepts custom presets.
paramstringURL search parameter that holds the moment (e.g. asof). Read on first connection (a deep link wins over value), written after every travel, removed when live; popstate travels with reason "history".
param-modereplace | pushreplacereplace keeps one history entry; push adds an entry per travel so Back/Forward walk through visited moments.
frameCSS selectorData regions to mark while in the past: data-as-of-past (tinted outline + an “As of …” tag drawn with ::before), aria-busy during a pending travel and aria-describedby pointing to the banner text. They get position: relative (zero specificity, your styles win).
scopeCSS selectordocumentRoot in which [data-as-of-live-only] elements are governed, including ones added later. Set it when a page has several instances.
layoutstack | barstackstack: three stacked rows (sidebar, panel). bar: three columns for the top of a dashboard (from 46rem of container width, stacked below), banner under the bar.
localeBCP 47en-USDate, time, relative-time formatting and first day of the week.
labelstringPoint in timeAccessible name of the three-row group.
history-sizenumber5How many recent destinations the Jump to menu keeps (0 disables them).
storage-keystringlocalStorage key to persist recent destinations across visits (failures are ignored).
settlenumber (ms)400Quiet time after the last step click before a single travel is emitted. Typed dates settle after 900 ms, or at once on Enter or blur; Escape discards a typed draft.
data-statelive | pastSet by the component. data-pending is present while waitUntil promises are pending, data-drafting while steps settle.
data-as-of-live-onlyboolean (on your elements)Elements in scope with this attribute become inert (plus data-as-of-locked for styling, dimmed by default) while viewing the past; their previous inert state is restored when you return or when the component is removed.

Properties

Name유형Description
dateDate | nullThe viewed moment (end of day with precision="day"), null when live (read-only).
livebooleanTrue while viewing live data (read-only).
departed{ value, date, live, at } | nullWhere you were before the last jump (the Left from row); at is when you left (read-only).
recentstring[]Recent destinations as normalized values, newest first. Settable (e.g. from your server).
pendingbooleanTrue while the last travel waits for its waitUntil promises (read-only).
presetsstring | Array<{ label, date: Date | string | (now: Date) => Date }>A string mirrors the attribute; an array replaces the built-in presets (e.g. fiscal periods, release dates, “Before the migration”).
stringsPartial<Record<string, string>>Overrides for every visible text and announcement (viewing, now, departed, live, past, liveTitle, liveSub, endOfDay, bannerText ({date}), bannerNote, bannerLocked, returnToNow, frameLabel ({date}), jump, presets, recent, goBack, goBackTo ({date}), stepBack / stepForward ({unit}), unitDay…, presetLastQuarter…, announceTravel ({date}), announceLive, announceMin, announceError ({date}, {current})…). English defaults.

Methods

NameDescription
travel(target, { reason? })Travel to a Date, a value string, or null / "now" for live. Emits mv-travel (cancelable), applies the moment, waits for waitUntil promises, then emits mv-arrive. Returns Promise<boolean> (false if cancelled, unchanged, superseded or failed).
returnToNow()Travel to live data (reason "return").
goBack()Travel to the Left from moment (reason "back"); calling it twice toggles between two moments.
step(n = -1, unit?)Step n units from the viewed moment (live counts as now); consecutive steps settle into one travel (reason "step").

Events

NameDescription
mv-travelCancelable, before a jump. detail: { from, to (Date | null = live), fromValue, toValue, live, reason: "step" | "input" | "preset" | "recent" | "back" | "return" | "history" | "api", waitUntil(promise) }. Refetch here and pass the promise to waitUntil: frames get aria-busy and a progress line runs; a rejection rolls the moment, Left from and recent back. preventDefault() refuses the jump.
mv-arriveThe jump is complete (after waitUntil promises resolved). detail: { from, to, fromValue, toValue, live, reason }. The arrival is announced politely.
mv-travel-errorA waitUntil promise rejected and the jump was rolled back. detail: { from, to, fromValue, toValue, reason, error }.

CSS classes

NameDescription
mv-as-of-bannerPast-mode region: -banner-icon, -banner-text (-banner-lead with <time class="mv-as-of-banner-time">, -banner-note) and .mv-as-of-return.
mv-as-of-consoleThe labeled group of three .mv-as-of-row[data-row="viewing | now | departed"], each with -head (.mv-as-of-label), -moment (<time class="mv-as-of-time"> + -sub) and -action. The viewing row gets data-past.
mv-as-of-controlsViewing row controls: -stepper (.mv-as-of-step[data-dir] + .mv-as-of-input), .mv-as-of-unit, .mv-as-of-jump; .mv-as-of-state is the Live / Past badge (data-tone), .mv-as-of-progress the pending line.
mv-as-of-menuJump to menu (Popover API top layer): -menu-group, -menu-heading, .mv-as-of-option (aria-current="date" on the viewed moment) with -option-check, -option-label, -option-sub.

CSS variables

NameDefaultDescription
--mv-as-of-tintvar(--mv-warning)Past-mode color: banner, Viewing row edge, Past badge, frame outline and tag (also read by frames outside the component).
--mv-as-of-livevar(--mv-success)Live color: Live badge and the breathing Now marker.
--mv-as-of-label-width6.75remWidth of the row label column in the stack layout.

Accessibility

The three rows are a labeled group (label, “Point in time” by default) of three nested groups named by their visible labels (Viewing, Now, Left from), each exposing its moment in a real <time datetime> element, so a screen reader reads “Viewing, group, Tue, Jun 30, 2026, End of day · 3 months ago”. Every control is a native element reachable with Tab: back / forward step buttons named “Back 1 month” / “Forward 1 month” (aria-disabled rather than disabled at the limits, so focus is never lost), a labeled date or date-time field (Enter commits, Escape discards the draft), a labeled step-size select, the Jump to button (aria-expanded, aria-controls) and “Go back”, whose accessible name includes the destination (“Go back to Monday, August 31, 2026”). The Jump to menu opens with focus on the current moment, moves with arrow keys, Home and End (roving tabindex), marks the viewed moment with aria-current="date" and a check icon, and closes on Escape or outside click, returning focus to its button. Each arrival is announced once in a polite live region (“Now viewing data as of Tuesday, June 30, 2026.” / “Back to live data.”), as are clamps to the earliest data and failed fetches with their rollback; the Now clock updates silently. The past is never signaled by color alone: the banner is a named region (“Historical view”) with words, a <time> and a Return to now button, the badge says Live or Past with a distinct icon, and framed regions get an “As of …” tag plus aria-describedby pointing to the banner text and aria-busy while data reloads. Live-only controls become inert (out of the tab order and the accessibility tree) and the banner says editing is paused; if a control that had focus disappears, focus moves to the date field. Reduced motion (OS or data-motion="reduce"): the Now marker stops breathing and the loading line becomes static. Forced colors: borders use CanvasText and the past edge, frames and progress use Highlight. Known limits: moments are local wall times of the viewer (no time-zone attribute); the native date field follows the browser's language rather than locale; while a travel waits for its waitUntil promises, framed regions are faded and aria-busy, so their small print is briefly below 4.5:1; without scope, every [data-as-of-live-only] of the document is governed, so set scope when a page has several instances.

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