エクスクルーシブ
Steady Pace <mv-steady-pace>
パーセンテージだけでなく、今日から必要な一定のペースを教えてくれる目標ペース管理です。貯蓄目標、売上ノルマ、読書や学習の目標、資金調達、スプリントのスコープ、トレーニング計画などに使えます。
文化的リファレンス
ウサギとカメ、イソップ(紀元前6世紀頃、寓話)。足の速いウサギは先を急ぎ、休んで負けてしまいますが、足の遅いカメは最後まで一定のペースを保って勝ちます。大事なのは速さではなく、ゴールに対するペースです。UI は開始から期限までの一定のペースに照らして進み具合を測り、今日から必要な一定のペースを伝え、進捗が一気に進んだあとに停滞したときには、それをやさしく指摘します。
仕組み
パーセンテージだけでなく、今日から必要な一定のペースを伝える目標ペース管理:貯蓄目標、販売ノルマ、読書や学習の目標、募金、スプリントのスコープ、トレーニング計画など。目標値、開始日、期限、日付付きのエントリー(金額、または entry-type="total" を指定した累計)を渡すと、現在地を開始から期限までの均等な「steady line」と比較します。単位と日数でどれだけ先行または遅れているか(「$194 behind the steady line」「4 days behind」)、今日から必要なペース(それに従えば必ず期限内に終わるよう切り上げ:「$54/day to reach $4,000 by Nov 1」)、直近 7 日間と 14 日間のペースで足りるかどうか(必要なペースに目盛りが付いた小さなメーターと言葉による判定)、そして最近のペースだとどこに着地するか(「you’d finish around Nov 15, 14 days after the deadline」)を示します。進捗の大半が 1、2 日に集中し、その後に静かな 1 週間が続いた場合は、それを伝える穏やかなメモと、ペースを一定に保つための 1 日あたりの少額の目安を示します。警告になることはありません。チャート版は、実際の累積進捗を階段状に描き、steady line、今日から必要なライン、最近のペースによる予測を完了日のラベル付きで描画します。ホバーで数値を読み取れ、開閉式の領域の奥には実際のデータテーブルがあります。what-if スライダーでは別の 1 日あたりの量を 2 つのルールで試せます:期限を維持する(その時点でどれだけ達成できるか)、または日付を動かす(いつ終わるか)。祝日を考慮した平日のみのカウント、日・週・月単位の表示、相対日付(「+37d」「-2w」)、深夜 0 時の更新に対応します。3 つのバリアント:インラインの文、カード、フルチャート。
| カテゴリー | データ表示 |
|---|---|
| タイプ | Web Component(<mv-steady-pace>) |
| ステータス | 安定版 |
| キット | 根拠を自ら語る数値 |
| 同時にインストールされるもの | slider |
| Keywords | exclusive, culture, pacing, goal, target, deadline, quota, savings, fundraising, burn-up, burndown, run-rate, required-rate, habit, streak, training-plan, sprint, what-if, chart, workdays, projection |
When to use
- A savings, fundraising or budget goal should say how much to put in per day or week from now on, not only how far along it is
- A sales team tracks a quarterly quota in workdays and needs to know if it is ahead or behind and what weekly pace closes the gap
- A reading, learning or training plan should flag progress that came in bursts and suggest a small steady daily amount instead
- A goals list or dashboard row needs a one-line pacing status with the required rate, in a sentence rather than a chart
Avoid when
- There is no deadline: the value only needs to show how far along a task is → use Progress instead
- The question is when an open-ended metric reaches a goal on its trend, with levers and an uncertainty range → use Yet To Come instead
- People repeat the same task and want to race their own previous attempt rather than a calendar → use Ghost Run instead
インストール
node scripts/add.mjs steady-pace --out ./src/marvelousMarvelous UI MCP サーバーを使う AI エージェント:install_components({ slugs: ["steady-pace"], target_dir: "<absolute path>/src/marvelous", framework: "react" })。
コピーされるファイル(依存関係を含む):tokens/tokens.css, core/base.css, core/dom.js, core/element.js, components/slider/slider.js, components/slider/slider.css, core/motion.js, core/observe.js, components/steady-pace/steady-pace.js, components/steady-pace/steady-pace.css。
使い方
クイックスタート、動作する最小限のマークアップ:
<mv-steady-pace label="Emergency fund" format="currency" target="4000" start="-45d" deadline="+37d">
<script type="application/json" data-entries>[["-44d", 300], ["-31d", 300], ["-17d", 300], ["-3d", 300]]</script>
</mv-steady-pace>基本となるマークアップです。ここから属性、data-*、CSS 変数でカスタマイズしてください:
<div id="sp-demo" style="width:min(100%,68rem);margin-inline:auto">
<style>
#sp-demo { display:grid; gap:1.75rem }
#sp-demo .spd-bar { display:flex; flex-wrap:wrap; align-items:center; justify-content:space-between; gap:.75rem 1rem }
#sp-demo .spd-bar h3 { margin:0; font-size:1.0625rem; letter-spacing:-.01em }
#sp-demo .spd-bar p { margin:.125rem 0 0; color:var(--mv-fg-muted); font-size:.8125rem }
#sp-demo .spd-actions { display:flex; flex-wrap:wrap; align-items:center; gap:.5rem .75rem }
#sp-demo .spd-col { display:grid; gap:.75rem; min-width:0; align-content:start }
#sp-demo .spd-row { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:1.5rem; align-items:start }
#sp-demo .spd-log { display:flex; align-items:center; gap:.5rem; min-width:0; color:var(--mv-fg-muted); font-size:.75rem }
#sp-demo .spd-log span:last-child { overflow:hidden; text-overflow:ellipsis; white-space:nowrap }
#sp-demo .spd-goals { margin:0; padding:0; list-style:none; border:1px solid var(--mv-border); border-radius:var(--mv-radius-xl); background:var(--mv-surface); box-shadow:var(--mv-shadow-xs) }
#sp-demo .spd-goals li { display:grid; grid-template-columns:minmax(9rem,13rem) minmax(0,1fr); align-items:center; gap:.5rem 1.25rem; padding:.875rem 1.125rem; font-size:.875rem }
#sp-demo .spd-goals li + li { border-top:1px solid var(--mv-border) }
#sp-demo .spd-goal { display:grid; gap:.125rem; min-width:0 }
#sp-demo .spd-goal strong { font-weight:600 }
#sp-demo .spd-goal span { color:var(--mv-fg-muted); font-size:.75rem }
@media (max-width:52rem) {
#sp-demo .spd-row { grid-template-columns:minmax(0,1fr) }
#sp-demo .spd-goals li { grid-template-columns:minmax(0,1fr) }
}
</style>
<!-- Main: a savings goal, full chart with a what-if -->
<section class="spd-col" aria-labelledby="spd-main-h">
<div class="spd-bar">
<div>
<h3 id="spd-main-h">Emergency fund</h3>
<p>Personal finance app · deposits since mid-August</p>
</div>
<div class="spd-actions">
<mv-segmented id="spd-view" aria-label="View" value="chart">
<button value="inline">Inline</button>
<button value="card">Card</button>
<button value="chart">Chart</button>
</mv-segmented>
<label class="mv-choice" data-control="end">
<input type="checkbox" role="switch" class="mv-switch" id="spd-workdays">
<span class="mv-choice-text"><span class="mv-choice-title">Workdays only</span></span>
</label>
<button class="mv-button" data-size="sm" type="button" id="spd-log-btn">Log $75 today</button>
</div>
</div>
<mv-steady-pace id="spd-main" label="Save $4,000 for an emergency fund" format="currency" target="4000" start="-45d" deadline="+37d" variant="chart" what-if="fixed">
<script type="application/json" data-entries>[
["-44d", 300, "Paycheck"], ["-38d", 180], ["-31d", 300, "Paycheck"], ["-27d", 150], ["-24d", 90],
["-20d", 100], ["-17d", 300, "Paycheck"], ["-12d", 120], ["-9d", 60], ["-3d", 300, "Paycheck"], ["-1d", 75]
]</script>
</mv-steady-pace>
<div class="spd-log" aria-hidden="true"><span class="mv-badge" data-variant="secondary" data-shape="pill" id="spd-event">mv-pace-change</span><span id="spd-last">Waiting for an event…</span></div>
</section>
<div class="spd-row">
<!-- A sales quota counted in workdays, shown per week; two big deals then a quiet stretch -->
<section class="spd-col" aria-labelledby="spd-quota-h">
<div class="spd-bar"><div><h3 id="spd-quota-h">Q4 sales quota</h3><p>Workdays only · pace per week · burst detection</p></div></div>
<mv-steady-pace label="Team quota" format="currency" target="180000" start="-24d" deadline="+60d" working-days per="week">
<script type="application/json" data-entries>[
["-22d", 42000, "Kestrel renewal"], ["-19d", 38500, "Globex expansion"], ["-15d", 4200], ["-9d", 6500], ["-6d", 2800], ["-2d", 3600]
]</script>
</mv-steady-pace>
</section>
<!-- A learning goal in custom units, with the "move the date" what-if -->
<section class="spd-col" aria-labelledby="spd-words-h">
<div class="spd-bar"><div><h3 id="spd-words-h">Spanish vocabulary</h3><p>Custom unit · what-if that moves the date</p></div></div>
<mv-steady-pace label="Learn 1,500 words" unit="words" target="1500" start="-20d" deadline="+10d" what-if="extend">
<script type="application/json" data-entries>[
["-20d", 60], ["-19d", 45], ["-18d", 70], ["-17d", 50], ["-15d", 80], ["-14d", 40], ["-13d", 55], ["-11d", 65],
["-10d", 50], ["-8d", 75], ["-7d", 60], ["-6d", 45], ["-4d", 90], ["-3d", 70], ["-2d", 55], ["-1d", 60], ["today", 70]
]</script>
</mv-steady-pace>
</section>
</div>
<!-- Inline sentences inside a goals list -->
<section class="spd-col" aria-labelledby="spd-goals-h">
<div class="spd-bar"><div><h3 id="spd-goals-h">Team goals</h3><p>Inline variant: one status and one sentence per row</p></div></div>
<ul class="spd-goals">
<li>
<span class="spd-goal"><strong>Sprint 42 scope</strong><span>Engineering · workdays</span></span>
<mv-steady-pace variant="inline" unit="points" target="40" start="-6d" deadline="+7d" working-days>
<script type="application/json" data-entries>[["-5d", 5], ["-4d", 3], ["-2d", 4], ["-1d", 4]]</script>
</mv-steady-pace>
</li>
<li>
<span class="spd-goal"><strong>Community fundraiser</strong><span>Riverside food bank</span></span>
<mv-steady-pace variant="inline" format="currency" target="25000" start="-30d" deadline="+30d">
<script type="application/json" data-entries>[["-29d", 6000, "Launch event"], ["-27d", 1200], ["-20d", 800], ["-14d", 650], ["-9d", 400], ["-4d", 750]]</script>
</mv-steady-pace>
</li>
<li>
<span class="spd-goal"><strong>Marathon training block</strong><span>Aiko Tanaka · distance</span></span>
<mv-steady-pace variant="inline" unit="km" target="400" start="-50d" deadline="+6d">
<script type="application/json" data-entries>[["-49d", 60], ["-40d", 70], ["-31d", 75], ["-22d", 80], ["-12d", 70], ["-3d", 45]]</script>
</mv-steady-pace>
</li>
</ul>
</section>
<script type="module">
await customElements.whenDefined("mv-steady-pace");
const main = document.getElementById("spd-main");
const badge = document.getElementById("spd-event");
const last = document.getElementById("spd-last");
main.addEventListener("mv-pace-change", (e) => {
const { tier, previous, statement, label } = e.detail;
if (previous === null) return;
badge.textContent = "mv-pace-change";
badge.dataset.variant = tier === "ahead" || tier === "done" ? "success" : tier === "on-pace" ? "info" : "warning";
last.textContent = `${previous} → ${tier}: ${label}. ${statement}`;
});
main.addEventListener("mv-whatif", (e) => {
badge.textContent = "mv-whatif";
badge.dataset.variant = "secondary";
last.textContent = e.detail.statement;
});
document.getElementById("spd-log-btn").addEventListener("click", () => main.add(75));
document.getElementById("spd-workdays").addEventListener("change", (e) => { main.workingDays = e.target.checked; });
document.getElementById("spd-view").addEventListener("mv-change", (e) => { main.variant = e.detail.value; });
</script>
</div>API
Attributes
| Name | タイプ | Default | Description |
|---|---|---|---|
target | number | The goal, in the same unit as the entries (e.g. 4000 for $4,000, 1500 words). Required. | |
deadline | date | Last day of the plan (inclusive). Accepts YYYY-MM-DD, any Date.parse string, "today" or a date relative to today: "+37d", "+6w", "+3m". Required. | |
start | date | First day of the plan, same formats. Defaults to the first entry’s date (or today). The steady line starts here. | |
start-value | number | 0 | Amount already there before any entry (e.g. money already saved). With entry-type="total", only used when no reading exists on or before the start. |
entry-type | amount | total | amount | amount: each entry adds to the total (deposits, pages read, deals closed). total: each entry is a reading of the running total (an account balance, a fundraiser page); several readings on one day keep the last. |
today | date | Pins “today” (server-rendered pages, reports as of a date, tests). Default: the user’s local date, refreshed at midnight while the page is open. | |
working-days | boolean | Counts Monday to Friday only (minus the holidays property): the steady line is flat on days off, gaps read “3 workdays behind” and the pace is per workday. | |
variant | inline | card | chart | card | inline: status chip + one sentence, for lists and table rows. card: the needed pace, a rail comparing goal done with today’s steady mark, recent pace and notes. chart: the card beside a chart with legend and a data table. |
what-if | fixed | extend | Adds the what-if slider (card and chart variants). fixed keeps the deadline and reports what you would have by then; extend lets the date move and reports when you would finish. Users can switch between the two with the built-in toggle, which updates this attribute. | |
per | day | week | month | day | Period in which rates are written (“$10,756/week”). With working-days, a week is 5 workdays and a month 21.75. |
label | string | Name of the goal, shown in the header and used as the accessible name of the group and in announcements. | |
format | number | currency | number | How amounts are written (Intl.NumberFormat). |
currency | ISO 4217 code | USD | Currency for format="currency". |
unit | string | Suffix for plain numbers (“words”, “km”, “points”); the headline reads “42 words/day”. | |
locale | BCP 47 tag | en-US | Locale for numbers and dates. |
decimals | number | Fixed number of decimals (default: automatic, fewer for large values). Paces are always rounded up at that precision. | |
tolerance | number (days) | 1 | Gap, in days of steady progress, under which you are “On pace” rather than ahead or behind. |
strain | number | 1.5 | When behind and the pace needed from today exceeds this multiple of the original steady rate, the tier becomes far-behind (stronger chip, same kind wording). |
recent-days | number | 14 | Window of the recent pace used for the projection; the card lists the last 7 days and this window. If nothing is logged today yet, the window ends yesterday so a morning never counts against you. |
data-tier / data-burst / data-whatif-touched / data-variant | set by the component | Current tier (not-started | ahead | on-pace | behind | far-behind | done | missed), present when a burst-then-stall pattern is detected, present once the what-if slider moved, resolved variant. |
Properties
| Name | タイプ | Description |
|---|---|---|
entries | Array<[date, amount, note?] | { date, amount, note? }> | Dated entries (any date format accepted by the attributes, including relative ones). Can also come from a child <script type="application/json" data-entries>. Reading returns [{ date: "YYYY-MM-DD", amount, note }]. |
holidays | Array<date> | Days skipped in working-days mode (also from <script type="application/json" data-holidays>). |
pace | object | null | Read-only snapshot, the same object as mv-pace-change’s detail: { tier, label, statement, current, target, remaining, start, deadline, today, per, steadyRate, steadyToday, gap: { units, days, workingDays }, required, daysLeft, recent: [{ days, from, to, pace, ratio, verdict }], projection: { pace, finish, daysVsDeadline }, reachedOn, burst: { days, share } | null }. Dates are YYYY-MM-DD, rates are per the per period. |
status | string | null | Current tier (read-only). |
whatIfPace | number | null | What-if amount per period. Set it to drive the slider from code; null follows the recent pace again. |
formatter | (value, { signed, rate }) => string | Replaces format / currency / unit / decimals for every amount. |
strings | Partial<Record<string, string>> | Overrides for every visible text and announcement, with {placeholders} (e.g. { behind: "{days} to catch up", day: "day", days: "days" }). English defaults; see the STRINGS table at the top of steady-pace.js for all keys. |
target / start / deadline / today / startValue / entryType / workingDays / variant / whatIf / per / … | reflected | Every attribute has a matching camelCase property. |
Methods
| Name | Description |
|---|---|
add(amount, date = "today", note?) | Logs an entry (a new total with entry-type="total") and recomputes. Returns the new pace snapshot. |
refresh() | Recomputes, e.g. after the day changed with a pinned today removed. |
Events
| Name | Description |
|---|---|
mv-pace-change | The status tier changed; also fired once after the first computation with previous: null. detail: the pace snapshot plus { previous }. |
mv-whatif | The what-if slider moved, its rule was switched, or whatIfPace was set. detail: { mode: "fixed" | "extend", pace, per, finish (YYYY-MM-DD | null), daysVsDeadline, amountAtDeadline, shortfall, statement }. |
Content structure
| Name | Description |
|---|---|
script[data-entries] / script[data-holidays] | Optional JSON children for plain-HTML use; the component’s own DOM is appended after them. |
CSS classes
| Name | Description |
|---|---|
mv-steady-pace-root[data-variant] / -head / -label / -chip[data-tier] | Frame, header, goal label and status chip (icon + words). |
mv-steady-pace-sentence | The one-line sentence of the inline variant. |
mv-steady-pace-need / -need-value / -need-per / -need-text | Headline: the pace needed from today, its period and the sentence under it. |
mv-steady-pace-rail / -rail-fill / -rail-gap / -rail-mark / -gap | Rail: goal done (fill), today’s steady mark and the hatched gap between them (ahead: solid stripes, behind: outlined stripes); the gap sentence below. |
mv-steady-pace-recent / -recent-row[data-verdict] / -meter / -verdict / -projection | Recent pace rows with a meter and a notch at the needed pace, the verdict word, then the projection sentence. |
mv-steady-pace-note | The burst-then-stall note. |
mv-steady-pace-chart / -plot / -actual / -steady / -needed / -recent-line / -whatif-line / -gap-area / -dot[data-kind] / -finish-label / -legend / -data / -table | Chart parts, markers, legend and the data table disclosure. |
mv-steady-pace-whatif / -modes / -mode / -whatif-input / -whatif-result | What-if group: rule toggle (native radios), native range input (class mv-slider) and result sentence. |
CSS variables
| Name | Default | Description |
|---|---|---|
--mv-steady-pace-actual | var(--mv-accent) | Actual progress: rail fill, actual line, recent-pace dots, meters. |
--mv-steady-pace-steady | var(--mv-fg-muted) | Steady line color. |
--mv-steady-pace-ahead | var(--mv-success) | Ahead and done tiers, the ahead part of the rail, “Enough” verdicts. |
--mv-steady-pace-behind | var(--mv-warning) | Behind tiers and the behind part of the rail. There is deliberately no red. |
--mv-steady-pace-on-pace | var(--mv-accent) | On-pace chip. |
--mv-steady-pace-whatif | var(--mv-fg) | What-if line, marker and label. |
--mv-steady-pace-chart-height | 13.5rem | Height of the plot area. |
--mv-steady-pace-padding | var(--mv-space-5) | Inner padding of the card. |
Accessibility
The element is a group named by its label. Every status is written in words next to an icon: the chip says “4 days behind”, “On pace” or “Goal reached”, the gap sentence says “$194 behind the steady line”, recent rows end with “Enough”, “Nearly there” or “Less than needed”, and the rail, meters and chart are aria-hidden because the same facts are in the text; colors only reinforce it, and the rail’s ahead and behind parts also differ by pattern (solid versus outlined stripes). The chart’s lines differ by dash pattern and weight, and its full equivalent is a real table (date, added, total, steady line, difference) behind a native <details> disclosure that everyone can open. A single polite live region speaks only when the tier changes (“Emergency fund: On pace. $49/day to reach $4,000 by Nov 1.”), never on the first render, a slider move or a routine update. The what-if uses native controls: a radio group for the rule (keep the deadline or move the date) and <input type="range"> with a visible label, an aria-valuetext with the unit and period, and aria-describedby pointing at the result sentence, so each step is read with its consequence. The burst note and every behind state are phrased as information, never blame, and no red is used. Reduced motion (OS setting or data-motion="reduce"): no chart draw-in, rail and meter changes jump instead of sliding. Forced colors: fills use Highlight, markers CanvasText, the chip keeps a visible outline.