ExklusivBeta
Steady Pace <mv-steady-pace>
Zielplanung, die dir die gleichmäßige Rate nennt, die ab heute nötig ist, nicht nur einen Prozentsatz: Sparziele, Verkaufsquoten, Lese- oder Lernziele, Fundraising, Sprint-Umfang, Trainingspläne. Gib ihr ein Ziel, einen Start, eine Deadline und datierte Einträge (Beträge oder laufende Summen mit entry-type="total"), und sie vergleicht, wo du stehst, mit der gleichmäßigen „steady line“ vom Start bis zur Deadline: voraus oder zurück in Einheiten und in Tagen („$194 behind the steady line“, „4 days behind“), das ab heute nötige Tempo, aufgerundet, damit du bei Einhaltung immer rechtzeitig fertig wirst („$54/day to reach $4,000 by Nov 1“), ob deine letzten 7 und 14 Tage ausreichen (kleine Anzeigen mit einer Kerbe beim nötigen Tempo und einem Urteil in Worten) und wo dein jüngstes Tempo landet („you’d finish around Nov 15, 14 days after the deadline“). Wenn der meiste Fortschritt an einem oder zwei Tagen kam, gefolgt von einer ruhigen Woche, sagt ein freundlicher Hinweis das und nennt den kleinen täglichen Betrag, der die Dinge gleichmäßig hält, nie eine Warnung. Die Chart-Variante zeichnet den kumulierten tatsächlichen Fortschritt als Treppe, die steady line, die ab heute nötige Linie und die Projektion des jüngsten Tempos mit beschriftetem Enddatum, mit einer Hover-Anzeige und einer echten Datentabelle hinter einer Disclosure. Ein Was-wäre-wenn-Slider testet einen anderen Tagesbetrag nach zwei Regeln: die Deadline halten (was du bis dahin hättest) oder das Datum verschieben (wann du fertig wärst). Zählung nur von Arbeitstagen mit Feiertagen, Anzeige pro Tag, Woche oder Monat, relative Daten („+37d“, „-2w“) und eine Aktualisierung um Mitternacht. Drei Varianten: ein Inline-Satz, eine Karte und ein vollständiges Chart.
| Kategorie | Datenanzeige |
|---|---|
| Typ | Web Component (<mv-steady-pace>) |
| Status | Beta |
| Kit | Zahlen, die sich selbst erklären |
| Installiert auch | 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
Installation
node scripts/add.mjs steady-pace --out ./src/marvelousKI-Agent mit dem MCP-Server von Marvelous UI: install_components({ slugs: ["steady-pace"], target_dir: "<absolute path>/src/marvelous", framework: "react" }).
Kopierte Dateien (inklusive Abhängigkeiten): tokens/tokens.css, core/base.css, core/dom.js, core/element.js, core/motion.js, core/observe.js, components/steady-pace/steady-pace.js, components/steady-pace/steady-pace.css, components/slider/slider.js, components/slider/slider.css.
Verwendung
Schnellstart mit dem kleinsten funktionierenden Markup:
<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>Referenz-Markup zum Starten und Anpassen mit Attributen, data-* und CSS-Variablen:
<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, "Northwind 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>Kulturelle Referenz
Der Hase und die Schildkröte, Äsop (um das 6. Jahrhundert v. Chr., Fabel). Der schnelle Läufer sprintet voraus, hält zum Ausruhen an und verliert, während die langsame Schildkröte den ganzen Weg über ein gleichmäßiges Tempo hält und gewinnt: Es zählt das Tempo im Verhältnis zur Ziellinie, nicht die Geschwindigkeit. Die UI misst dich an diesem gleichmäßigen Tempo vom Start bis zur Deadline, sagt dir, welche konstante Rate ab heute nötig ist, und weist sanft darauf hin, wenn der Fortschritt in Schüben kam, gefolgt von Stillstand.
API
Attributes
| Name | Typ | 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 | Typ | 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.