익스클루시브

Yet To Come <mv-yet-to-come>

다음에 일어날 일을 바꾸도록 설계된, 세 부분으로 이루어진 이야기로 전하는 지표입니다. Past(스파크라인, 기간 동안의 변화, 주석이 달린 가장 큰 상승이나 하락), Now(현재 값, 이전 기간 대비 증감, 기간별 추세), Ahead(현재 추세가 이어질 경우 지표가 도달할 지점, 오늘은 0이고 기간마다 넓어지는 명확히 표시된 불확실성 범위)로 구성됩니다.

문화적 레퍼런스

크리스마스 캐럴, 찰스 디킨스 (1843, 도서). 세 유령이 한 남자에게 그의 과거, 현재, 그리고 아무것도 바뀌지 않으면 다가올 미래를 보여 주고, 그 미래를 본 것이 그가 삶의 방향을 바꾸게 만듭니다. UI는 지표를 같은 방식으로 전달합니다. 과거, 현재, 그리고 현재 추세대로의 미래를 보여 주고, 사용자에게 그 미래가 일어나기 전에 바뀌는 모습을 볼 수 있는 레버를 건넵니다.

작동 방식

다음에 일어날 일을 바꾸도록 만든, 세 부분으로 이루어진 이야기로 전달하는 지표입니다. Past(스파크라인, 기간 동안의 변화, 가장 큰 상승 또는 하락과 그 주석), Now(현재 값, 이전 기간 대비 증감, 기간별 추세), Ahead(현재 추세가 계속될 경우 지표가 도달할 지점과, 오늘은 0이고 기간마다 넓어지는 명확한 레이블의 불확실성 밴드). 타임스탬프가 있는 이력을 주면 직선 또는 복리 추세를 직접 적합하며, project(ctx)로 자체 모델을 연결할 수도 있습니다. 선택적 목표값은 예측을 평이한 문장으로 바꾸고(“Reaches $1M ARR around Jan 2028”, “Cash runs out around Feb 2028”, 가능성 높은 날짜 범위 포함), 앱이 정의한 1개에서 3개의 레버(네이티브 range 입력: 이탈률, 주간 가입자 수, 지출 삭감, 추가 절감…)가 미래를 실시간으로 다시 그립니다. 변하지 않은 추세는 점선 고스트로 남고, 차이가 명시됩니다(“3 months sooner than on the current trend”, “instead of after Mar 2028”). Ahead 패널은 언제나 가정을 문장으로 밝히며, 선은 색상만이 아니라 선 패턴으로 구분되고(실선은 실제값, 파선은 예측, 점선은 현재 추세), 좁은 컨테이너에서는 레이아웃이 분할 보기에서 접근 가능한 탭으로 전환됩니다.

카테고리데이터 표시
유형Web Component (<mv-yet-to-come>)
상태안정
키트스스로 설명하는 숫자
함께 설치되는 항목button, slider
Keywordsexclusive, culture, forecast, projection, trend, what-if, scenario, kpi, metrics, goal, target, runway, burn-rate, savings, sparkline, uncertainty, confidence-band, levers, dashboard, saas

When to use

  • A SaaS dashboard should show when ARR, users or MRR reach a goal on the current trend and what a lower churn would change
  • A finance screen must show cash runway and let founders test a spending cut or a bridge round before deciding
  • A savings, fitness or reading-streak app should turn a goal into a date and show what one extra weekly effort would buy
  • A capacity or budget report (storage, carbon, headcount) needs a forecast with an honest range and its assumptions in words

Avoid when

  • The data only needs to be plotted and explored, without a projection, a goal or levers → use Chart instead
  • Several discrete scenarios with many inputs must be compared side by side → use Fork instead
  • The series is seasonal, cyclical or driven by one-off events: a trend line would promise a false future unless project() models it

설치

node scripts/add.mjs yet-to-come --out ./src/marvelous

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

복사되는 파일(의존성 포함): tokens/tokens.css, core/base.css, components/button/button.css, core/dom.js, core/element.js, components/slider/slider.js, components/slider/slider.css, core/focus.js, core/motion.js, core/observe.js, components/yet-to-come/yet-to-come.js, components/yet-to-come/yet-to-come.css.

사용법

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

<mv-yet-to-come label="Annual recurring revenue" format="currency" target="1000000">
  <script type="application/json" data-history>[["2026-04-01", 812000], ["2026-05-01", 818000], ["2026-06-01", 822000], ["2026-07-01", 829000], ["2026-08-01", 834000], ["2026-09-01", 840360]]</script>
</mv-yet-to-come>

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

<div id="ytc-demo" style="width:min(100%,70rem);margin-inline:auto">
  <style>
    #ytc-demo { display:grid; gap:1.5rem }
    #ytc-demo .ytc-bar { display:flex; flex-wrap:wrap; align-items:center; justify-content:space-between; gap:.75rem 1rem }
    #ytc-demo .ytc-bar h3 { margin:0; font-size:1.0625rem; letter-spacing:-.01em }
    #ytc-demo .ytc-bar p { margin:.125rem 0 0; color:var(--mv-fg-muted); font-size:.8125rem }
    #ytc-demo .ytc-actions { display:flex; flex-wrap:wrap; align-items:center; gap:.5rem .75rem }
    #ytc-demo .ytc-row { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:1.5rem; align-items:start }
    #ytc-demo .ytc-col { display:grid; gap:.75rem; min-width:0 }
    #ytc-demo .ytc-log { display:flex; align-items:center; gap:.5rem; color:var(--mv-fg-muted); font-size:.75rem; min-width:0 }
    #ytc-demo .ytc-log span:last-child { overflow:hidden; text-overflow:ellipsis; white-space:nowrap }
    @media (max-width:52rem) { #ytc-demo .ytc-row { grid-template-columns:minmax(0,1fr) } }
  </style>

  <section class="ytc-col" aria-labelledby="ytc-arr-h">
    <div class="ytc-bar">
      <div>
        <h3 id="ytc-arr-h">Revenue outlook</h3>
        <p>Kestrel Analytics · B2B plan at $149 per customer per month</p>
      </div>
      <div class="ytc-actions">
        <label class="mv-choice" data-control="end">
          <input type="checkbox" role="switch" class="mv-switch" id="ytc-band" checked>
          <span class="mv-choice-text"><span class="mv-choice-title">Range band</span></span>
        </label>
        <button class="mv-button" data-variant="outline" data-size="sm" type="button" id="ytc-close">Close October</button>
      </div>
    </div>
    <mv-yet-to-come id="ytc-arr" label="Annual recurring revenue" format="currency" target="1000000" target-label="$1M ARR" horizon="18" period="month">
      <script type="application/json" data-history>[
        ["2025-09-01", 712000], ["2025-10-01", 731000], ["2025-11-01", 744000], ["2025-12-01", 752000],
        ["2026-01-01", 761000], ["2026-02-01", 768000], ["2026-03-01", 801000, "Team plan launched"],
        ["2026-04-01", 812000], ["2026-05-01", 818000], ["2026-06-01", 822000], ["2026-07-01", 829000],
        ["2026-08-01", 834000], ["2026-09-01", 840360]
      ]</script>
      <script type="application/json" data-levers>[
        { "id": "churn", "label": "Monthly churn", "min": 0.01, "max": 0.05, "step": 0.001, "value": 0.03, "format": "percent", "decimals": 1 },
        { "id": "signups", "label": "New customers per week", "min": 1, "max": 8, "step": 0.1, "value": 3.7, "decimals": 1 }
      ]</script>
    </mv-yet-to-come>
    <div class="ytc-log" aria-hidden="true"><span class="mv-badge" data-variant="secondary" data-shape="pill" id="ytc-status">mv-projection</span><span id="ytc-last">Waiting for the first projection…</span></div>
  </section>

  <div class="ytc-row">
    <section class="ytc-col" aria-labelledby="ytc-cash-h">
      <div class="ytc-bar"><div><h3 id="ytc-cash-h">Runway</h3><p>Built-in straight-line trend, a limit to stay clear of</p></div></div>
      <mv-yet-to-come id="ytc-cash" label="Cash in the bank" format="currency" target="0" target-label="$0" target-mode="avoid" horizon="24">
        <script type="application/json" data-history>[
          ["2025-09-30", 2460000], ["2025-10-31", 2388000], ["2025-11-30", 2312000], ["2025-12-31", 2241000],
          ["2026-01-31", 2152000], ["2026-02-28", 2074000], ["2026-03-31", 1981000, "Two senior hires"],
          ["2026-04-30", 1897000], ["2026-05-31", 1809000], ["2026-06-30", 1724000], ["2026-07-31", 1638000],
          ["2026-08-31", 1553000], ["2026-09-30", 1471000]
        ]</script>
        <script type="application/json" data-levers>[
          { "id": "cut", "label": "Cut monthly spend by", "min": 0, "max": 40000, "step": 1000, "value": 0, "format": "currency", "effect": "add" },
          { "id": "bridge", "label": "Bridge round", "min": 0, "max": 750000, "step": 25000, "value": 0, "format": "currency", "effect": "shift" }
        ]</script>
      </mv-yet-to-come>
    </section>

    <section class="ytc-col" aria-labelledby="ytc-save-h">
      <div class="ytc-bar"><div><h3 id="ytc-save-h">Savings goal</h3><p>Weekly data, compact tabs layout</p></div></div>
      <mv-yet-to-come id="ytc-save" label="Emergency fund" format="currency" target="10000" horizon="26" panel="past" layout="tabs">
        <script type="application/json" data-history>[
          ["2026-06-07", 3120], ["2026-06-14", 3260], ["2026-06-21", 3410], ["2026-06-28", 3395, "Car repair"],
          ["2026-07-05", 3540], ["2026-07-12", 3700], ["2026-07-19", 3830], ["2026-07-26", 3990],
          ["2026-08-02", 4105], ["2026-08-09", 4280], ["2026-08-16", 4400], ["2026-08-23", 4560],
          ["2026-08-30", 4690], ["2026-09-06", 4850], ["2026-09-13", 4975], ["2026-09-20", 5140]
        ]</script>
        <script type="application/json" data-levers>[
          { "id": "extra", "label": "Extra per week", "min": 0, "max": 150, "step": 5, "value": 0, "format": "currency", "effect": "add", "description": "Set aside on top of your usual deposits." }
        ]</script>
      </mv-yet-to-come>
    </section>
  </div>

  <script type="module">
    await customElements.whenDefined("mv-yet-to-come");
    const arr = document.getElementById("ytc-arr");
    const cash = document.getElementById("ytc-cash");
    const PRICE = 149;

    // The app's own model: customers churn and sign up each month; ARR = 12 × MRR.
    arr.project = ({ history, levers, steps }) => {
      let customers = history.at(-1).v / (PRICE * 12);
      const perMonth = (levers.signups * 52) / 12;
      return {
        points: steps.map(() => {
          customers = customers * (1 - levers.churn) + perMonth;
          return customers * PRICE * 12;
        }),
        assumptions: `Every customer pays $${PRICE} per month; ARR is 12 times the monthly revenue of active customers.`,
      };
    };

    cash.strings = { avoidHit: "Cash runs out around {date}", avoidClear: "Cash lasts past {end}" };

    const status = document.getElementById("ytc-status");
    const last = document.getElementById("ytc-last");
    arr.addEventListener("mv-projection", (e) => {
      const { status: s, statement } = e.detail;
      status.textContent = s;
      status.dataset.variant = s === "reached" ? "success" : "secondary";
      last.textContent = statement;
    });

    document.getElementById("ytc-band").addEventListener("change", (e) => {
      for (const el of document.querySelectorAll("#ytc-demo mv-yet-to-come")) el.band = e.target.checked ? "true" : "false";
    });

    // New actuals arrive: the story moves one month forward.
    const close = document.getElementById("ytc-close");
    close.addEventListener("click", () => {
      if (close.getAttribute("aria-disabled") === "true") return;
      const h = arr.history;
      const t = new Date(h.at(-1).t);
      t.setUTCMonth(t.getUTCMonth() + 1);
      arr.history = [...h, { t, v: h.at(-1).v + 6100 }];
      close.setAttribute("aria-disabled", "true"); // not disabled: the focus stays on the button
      close.textContent = "October closed";
    });
  </script>
</div>

API

Attributes

Name유형DefaultDescription
labelstringName of the metric (“Annual recurring revenue”), shown in the header and used as the accessible name of the group and of the tabs.
formatnumber | currency | percentnumberHow values are written (Intl.NumberFormat). Values of 10,000 and more are shown in compact notation ($840K, 1.2M). percent expects fractions (0.42 = 42%).
currencyISO 4217 codeUSDCurrency for format="currency" (also the default of currency levers).
unitstringSuffix appended to every value (“t CO₂e”, “GB”, “days”). Ignored for percent.
decimalsnumberFixed number of decimals for values under 10,000 (default: automatic).
localeBCP 47 tagen-USLocale for numbers and dates (dates are read and written in UTC).
periodday | week | month | quarter | yearStep of the series and of the projection (months, quarters and years follow the calendar). Guessed from the median gap between history points when omitted.
horizonnumber (periods)How many periods ahead to project. Defaults to the length of the history (3 to 60).
fit-windownumber (points)Only the last N history points feed the built-in trend and the range (e.g. 6 to follow a recent change of pace). Default: all.
modellinear | exponential | autolinearBuilt-in trend: a straight line (least squares, continuing from today’s actual value) or compound growth (log-linear fit, positive values only). auto keeps compound growth only when it fits clearly better. Also sizes the range band when project() is used.
targetnumberGoal or limit. The Ahead panel then says when the projection crosses it (interpolated between periods), the likely date range from the band, and draws a target line with a crossing marker.
target-labelstringWording of the target in sentences and on the chart (“$1M ARR”, “$0”). Default: the formatted value.
target-modereach | avoidreachreach: crossing is the goal (“Reaches … around …”, success outcome). avoid: crossing is the danger (“Hits … around …”, danger outcome; “Stays clear of … through …” otherwise), for runway, budgets and quotas.
band"true" | "false"true"false" hides the uncertainty band, its legend entry, the date range and its sentence in the assumptions.
confidencenumber (0.5..0.99)0.8Coverage of the band. It is a random-walk-with-drift range: the spread of past period-to-period changes around the trend, zero today and widening with the square root of the distance plus the uncertainty on the trend itself.
layoutauto | split | tabs | stackautosplit: Past and Now stacked on the left, Ahead on the right. tabs: one panel at a time behind an ARIA tablist. stack: all three in one column. auto picks tabs when the element is narrower than 640px, split otherwise.
panelpast | present | futurefutureSelected panel in the tabs layout (reflects the user’s choice).
heading-level1..63Level of the three panel headings, to fit the page outline.
data-layout / data-reveal / data-changed / data-pendingset by the componentResolved layout; story reveal state (pending until first seen, then done); present while a lever differs from its starting value; present while an async project() is running (aria-busy is set too).

Properties

Name유형Description
historyArray<[t, v, note?] | { t, v, note? }>Timestamped values (Date, ISO string or epoch ms). Sorted for you; a note marks the point in the Past panel and names the biggest jump when it falls on it. Can also come from a child <script type="application/json" data-history>. Reading it returns [{ t: Date, v, note }].
leversArray<Lever>Lever = { id, label, min = 0, max = 100, step, value, format?: "number" | "currency" | "percent" | (v) => string, decimals?, unit?, currency?, description?, assumption? (template with {label} {value} {base}), effect? }. value "auto" starts a rate lever at the fitted trend. effect (built-in model only): add (value added per period, default), rate (replaces the trend per period, or the growth rate for exponential), scale (multiplies the trend), shift (one-time amount added from the next period). Also from <script type="application/json" data-levers>. Setting them resets their values.
valuesRecord<string, number>Current lever values. Setting merges and clamps (e.g. restore a saved scenario); it re-projects and announces like a user change.
project(ctx) => number[] | Point[] | { points, assumptions? } | Promise<…>Your model. ctx = { history, levers (values by id), steps (Date of each future period), period, horizon, baseline (true when computing the unchanged scenario), fit: { model, rate, slope } }. Return one value per step (or { v, lo?, hi? } to supply your own range; missing ranges are sized by the built-in fit). assumptions is a sentence prepended to the lever list. Async results show a pending state; stale ones are dropped.
momentsArray<{ t, label }>Extra annotated moments (a launch, a price change) marked on the sparkline; the one inside the biggest jump names it. Also from <script type="application/json" data-moments>.
formatter(value, { compact, signed, precise }) => stringReplaces format / currency / unit / decimals for every value.
stringsPartial<Record<string, string>>Overrides for every visible text and announcement, with {placeholders} (e.g. { avoidHit: "Cash runs out around {date}", avoidClear: "Cash lasts past {end}" }). Keys: past, present, future, futureIdle, futureChanged, tabs, empty, pastUp, pastDown, pastFlat, momentUp, momentDown, onDate, inDate, asOf, versusPrev, trend, perPeriod, reach, notReached, already, avoidHit, avoidClear, endpoint, rangeDates, rangeOpen, rangePossible, rangeValue, sooner, later, same, insteadOfNever, insteadOf, versus, assumptions, assumeLinear, assumeExp, assumeCustom, assumeLever, assumeLeverChanged, assumeBand, assumeNoBand, levers, reset, leverNow, legendActual, legendProjection, legendBand, legendBaseline, legendTarget, now, projected, tablePast, tableFuture, tableDate, tableValue, tableRange, busy, count ({n} {unit}), unitDay, unitDays, unitWeek, unitWeeks, unitMonth, unitMonths, unitQuarter, unitQuarters, unitYear, unitYears (period words; the singular or plural form follows Intl.PluralRules for locale), adjDay, adjWeek, adjMonth, adjQuarter, adjYear (“monthly”…), quarterDate (“Q{q} {year}”). English defaults.
projectionobject | nullLatest result, read-only: the same object as mv-projection’s detail.
label / format / target / horizon / …reflectedEvery attribute has a matching property (camelCase: fitWindow, targetLabel, targetMode, headingLevel).

Methods

NameDescription
setLever(id, value)Sets one lever (clamped to its range) and re-projects.
resetLevers()Puts every lever back to its starting value (the “Reset” button does the same).
refresh()Re-runs the fit and the projection, e.g. after inputs of your project() changed outside the levers.
show(panel)Selects "past", "present" or "future" in the tabs layout.

Events

NameDescription
mv-leverCancelable, on every input of a lever. detail: { id, value, previous, values }. preventDefault() puts the input back (e.g. a value your plan does not allow).
mv-projectionAfter every projection, including the first. detail: { points: [{ t: Date, v, lo, hi }] (index 0 = today’s actual value), baseline (the unchanged-trend points when a lever moved, else null), status: "reached" | "not-reached" | "already" | "none", crossing: { date, earliest, latest, periods } | null, statement, range, assumptions, changed, levers }.

Content structure

NameDescription
script[data-history] / [data-levers] / [data-moments]Optional JSON children for plain-HTML use; the component’s own DOM is appended after them.

CSS classes

NameDescription
mv-yet-to-come-root / -header / -label / -tabs / -tab / -panelsCard frame, header with the metric label and the tablist (tabs layout only).
mv-yet-to-come-panel[data-panel]past | present | future panels; -panel-head, -title (hollow, filled or dashed marker), -sub, -asof.
mv-yet-to-come-spark / -summary / -momentPast panel: sparkline with start, end, note and key-moment dots; summary sentences.
mv-yet-to-come-value / -delta[data-direction] / -trendNow panel: big value, delta chip with arrow icon and sign, trend per period.
mv-yet-to-come-statement[data-outcome] / -outcome / -range / -chart / -plot / -legend / -assumptionsAhead panel: outcome sentence (good | bad | neutral, with an icon), date or value range, chart (-history, -projection, -baseline, -band, -target-line, -now-line paths; -dot[data-kind], -target-label, -cross-label, -chip hover readout), legend and assumptions.
mv-yet-to-come-levers / -lever[data-changed] / -lever-input / -lever-notchLever group; each lever is a native <input type="range" class="mv-slider">, and the notch marks its starting value once moved.

CSS variables

NameDefaultDescription
--mv-yet-to-come-actualvar(--mv-fg)Color of the actual (past) line and its area.
--mv-yet-to-come-projectionvar(--mv-accent)Color of the projection, band edges, Ahead tint and changed lever values.
--mv-yet-to-come-bandprojection at 15%Fill of the uncertainty band.
--mv-yet-to-come-targetvar(--mv-fg-muted)Target line and label.
--mv-yet-to-come-good / -badvar(--mv-success) / var(--mv-danger)Outcome colors (always paired with an icon and words).
--mv-yet-to-come-chart-height13remHeight of the Ahead chart.
--mv-yet-to-come-paddingvar(--mv-space-5)Inner padding of the panels.

Accessibility

The element is a labelled group (aria-labelledby the metric label) and every panel carries a real heading (heading-level) plus a text summary that stands on its own: Past says where the metric went and its biggest move, Now gives the value, the delta with its sign and the trend in words, Ahead gives the outcome sentence, the range and the assumptions. Charts are SVG with aria-hidden; each has a visually hidden table equivalent (date, value, and the range for projected points, sampled to 24 rows), and the hover readout is a pointer-only extra. Levers are native <input type="range"> elements with a <label>, an optional description linked by aria-describedby, and aria-valuetext set to the formatted value (“2.0%”), so arrows, Page Up/Down and Home/End work everywhere; the visible value is aria-hidden to avoid double reading. Projection changes are announced once in a polite live region, 700 ms after the last lever move (“Reaches $1M ARR around Jan 2028, instead of after Mar 2028. As early as …”), never on the initial render. In the tabs layout the panel switcher is a proper tablist (roving tabindex, arrows, Home/End, automatic activation, aria-controls/aria-selected) and panels are tabpanels labelled by their tab; in the split and stack layouts the panels are labelled groups. Nothing relies on color alone: actual, projection and current trend are solid, dashed and dotted lines (the legend repeats the patterns), the outcome has an icon and words, the delta has an arrow and a sign, panel markers differ by shape. The async state sets aria-busy. Reduced motion (OS or data-motion="reduce"): no story reveal, no line glide, tab panels switch instantly. Forced colors: lines map to CanvasText, Highlight and GrayText, the band keeps its dashed edges, the selected tab gets a Highlight outline. Known limits: the built-in model is a straight line or compound growth only (seasonal or event-driven series need your own project()); the chart hover readout has no keyboard equivalent, the hidden tables carry the same values; period words follow Intl.PluralRules with one singular and one plural string, so languages with more plural forms need a custom count template.

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