익스클루시브

Net Progress <mv-net-progress>

총 활동량과 순수 진척을 구분하고 반복 수정(churn)을 보이게 합니다. 문서와 사양서, 코드, 디자인 파일, 모델, 백로그, 콘텐츠 캘린더, 협상 중인 계약서에 적합합니다.

문화적 레퍼런스

오디세이아, 호메로스 (기원전 8세기경, 서사시). 페넬로페는 낮에는 수의를 짜고 밤에는 몰래 풀어 버립니다. 그래서 눈에 보이는 많은 작업이 아무런 진척도 만들어 내지 않습니다. UI에서는 총 활동량과 순 진척을 분리합니다. 이전 작업을 되돌리는 모든 편집은 그 작업이 만들어진 기간에서 차감되고, 이전 버전으로 반복해서 돌아가는 경우는 따로 짚어 주므로, 바빴지만 아무 진척이 없던 주가 누구도 탓하지 않고 드러납니다.

작동 방식

문서와 사양서, 코드, 디자인 파일, 모델, 백로그, 콘텐츠 캘린더, 협상 중인 계약서를 대상으로 총 활동량과 순 진척을 구분하고 변경 반복(churn)을 드러냅니다. 타임스탬프가 있는 편집(추가, 삭제, 되돌림, 재개, 선택적으로 영역과 버전 포함)을 전달하면, 모든 철회는 그것이 되돌리는 작업이 이루어진 기간에서 차감됩니다. 가장 최근 작업부터, 선택적인 기간 창 안에서("words still there after 14 days") 차감되므로 각 주, 일, 월마다 무엇이 만들어졌고 그중 무엇이 아직 남아 있는지 보여 줍니다. 분석 도구에서 미리 계산한 총/순 기간 데이터를 전달할 수도 있습니다. 기간마다 짝을 이룬 막대를 그리고(작성량 막대는 철회된 부분을 빗금으로, 그 옆에 유지된 양을, 아직 안정되지 않은 기간은 점선으로 표시), 선택한 기간을 10개의 점과 함께 한 문장으로 바꿉니다(“For every 10 words written this week, 3 remain so far.”). 어떤 영역이 이미 가졌던 버전으로 돌아가면 오락가락을 감지하고(“Pricing & plans went back and forth between two versions, 3 times.”, 버전 기록 $49/mo → $59/mo → $49/mo 포함), 변경 반복이 가장 많은 영역을 필터와 바로가기 링크와 함께 순위로 보여 주며, 최근 완료된 기간들의 churn에 대한 최소제곱 적합으로 추세를 탐색 중, 수렴 중, 안정 중 하나로 중립적으로 표현하고, 선택적으로 계획과 비교합니다(expect="converge"). 실제 데이터 표가 펼치기 영역 뒤에 있으며, 어디에서도 특정인을 지목하거나 탓하지 않습니다.

카테고리데이터 표시
유형Web Component (<mv-net-progress>)
상태안정
Keywordsexclusive, culture, churn, rework, net-progress, gross-vs-net, productivity, analytics, oscillation, flip-flop, revisions, backlog, reopened, code-churn, document, contract, negotiation, trend, bar-chart, data-table, dashboard

When to use

  • A spec, doc or contract draft sees lots of editing and the team needs to know how much of the work actually stuck
  • A backlog closes many tickets but reopened ones should count against the week they were closed, not vanish in a total
  • Code or design analytics already compute added vs still-there-after-N-days and need a readable weekly summary
  • A lead wants to see whether a project is still exploring or converging, and which sections keep flipping back

Avoid when

  • The question is whether a goal will be met by a deadline and what daily pace is needed → use Steady Pace instead
  • Readers must see which exact words of a text changed since a baseline, and who changed them → use Theseus instead
  • One metric over time needs a plain line, area or bar chart with no churn semantics → use Chart instead

설치

node scripts/add.mjs net-progress --out ./src/marvelous

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

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

사용법

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

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

  <!-- A product spec: words written vs words still there after 14 days -->
  <section class="npd-col" aria-labelledby="npd-spec-h">
    <div class="npd-bar">
      <div>
        <h3 id="npd-spec-h">Checkout 2.0 launch spec</h3>
        <p>Shared product doc · 5 editors · a take-back within 14 days counts as churn</p>
      </div>
      <div class="npd-actions">
        <mv-segmented id="npd-period" aria-label="Group by" value="week">
          <button value="week">Weeks</button>
          <button value="day">Days</button>
        </mv-segmented>
        <label class="mv-choice" data-control="end">
          <input type="checkbox" role="switch" class="mv-switch" id="npd-expect">
          <span class="mv-choice-text"><span class="mv-choice-title">Plan says converge</span></span>
        </label>
        <button class="mv-button" data-variant="outline" data-size="sm" type="button" id="npd-revert">Change pricing back to $59/mo</button>
      </div>
    </div>
    <mv-net-progress id="npd-spec" label="Checkout 2.0 launch spec" unit="word|words" window="14d">
      <script type="application/json" data-events>[
        {"at":"-52d","area":"overview","added":900},
        {"at":"-51d","area":"onboarding","added":1400},
        {"at":"-49d","area":"pricing","added":500,"version":"$49/mo"},
        {"at":"-45d","area":"onboarding","removed":900,"added":1200},
        {"at":"-43d","area":"overview","removed":300,"added":350},
        {"at":"-41d","area":"pricing","removed":350,"added":420,"version":"$59/mo"},
        {"at":"-38d","area":"onboarding","removed":800,"added":900},
        {"at":"-37d","area":"api","added":700,"version":"1,000 req/min"},
        {"at":"-35d","area":"pricing","removed":380,"added":400,"version":"$49/mo"},
        {"at":"-31d","area":"onboarding","removed":500,"added":600},
        {"at":"-29d","area":"api","removed":300,"added":350,"version":"600 req/min"},
        {"at":"-27d","area":"pricing","removed":300,"added":320,"version":"$59/mo"},
        {"at":"-24d","area":"onboarding","removed":250,"added":400},
        {"at":"-23d","area":"security","added":800},
        {"at":"-21d","area":"api","removed":200,"added":260,"version":"1,000 req/min"},
        {"at":"-17d","area":"security","removed":150,"added":300},
        {"at":"-16d","area":"onboarding","removed":120,"added":250},
        {"at":"-15d","area":"pricing","removed":280,"added":300,"version":"$49/mo"},
        {"at":"-13d","area":"glossary","added":300},
        {"at":"-14d","area":"security","removed":120,"added":90},
        {"at":"-10d","area":"security","removed":80,"added":450},
        {"at":"-9d","area":"onboarding","removed":40,"added":200},
        {"at":"-8d","area":"overview","added":150},
        {"at":"-7d","area":"security","removed":110},
        {"at":"-6d","area":"api","added":120},
        {"at":"-3d","area":"security","removed":30,"added":380},
        {"at":"-2d","area":"onboarding","added":160},
        {"at":"-1d","area":"overview","removed":20,"added":120},
        {"at":"-1d","area":"onboarding","removed":70},
        {"at":"-8h","area":"security","removed":90,"added":40},
        {"at":"-2h","area":"pricing","added":90}
      ]</script>
      <script type="application/json" data-areas>[
        {"id":"overview","label":"Overview & goals","href":"#spec-overview"},
        {"id":"onboarding","label":"Onboarding flow","href":"#spec-onboarding"},
        {"id":"pricing","label":"Pricing & plans","href":"#spec-pricing"},
        {"id":"api","label":"API rate limits","href":"#spec-api"},
        {"id":"security","label":"Security review","href":"#spec-security"},
        {"id":"glossary","label":"Glossary","href":"#spec-glossary"}
      ]</script>
    </mv-net-progress>
    <div class="npd-log" aria-hidden="true"><span class="mv-badge" data-variant="secondary" data-shape="pill" id="npd-event">event</span><span id="npd-last">Pick a week, or an area in “Most rework”…</span></div>
  </section>

  <div class="npd-row">
    <!-- A sprint backlog: tickets closed vs tickets that stayed closed -->
    <section class="npd-col" aria-labelledby="npd-bl-h">
      <div class="npd-bar"><div><h3 id="npd-bl-h">Payments team backlog</h3><p>Compact · reopened tickets count against the week they were closed</p></div></div>
      <mv-net-progress variant="compact" label="Sprint tickets" unit="ticket|tickets" gross-label="Closed" net-label="Stayed closed" net-verb="stayed closed" window="21d">
        <script type="application/json" data-events>[
          {"at":"-51d","area":"checkout","added":9},
          {"at":"-49d","area":"search","added":6},
          {"at":"-48d","area":"notifications","added":4},
          {"at":"-44d","area":"checkout","reopened":4,"added":7},
          {"at":"-43d","area":"search","reopened":2,"added":5},
          {"at":"-41d","area":"billing","added":6},
          {"at":"-38d","area":"checkout","reopened":3,"added":8},
          {"at":"-36d","area":"billing","reopened":3,"added":5},
          {"at":"-35d","area":"search","added":4},
          {"at":"-30d","area":"checkout","reopened":4,"added":6},
          {"at":"-29d","area":"notifications","reopened":1,"added":5},
          {"at":"-27d","area":"billing","reopened":2,"added":4},
          {"at":"-24d","area":"checkout","reopened":3,"added":7},
          {"at":"-22d","area":"search","reopened":1,"added":6},
          {"at":"-20d","area":"billing","added":5},
          {"at":"-16d","area":"checkout","reopened":4,"added":6},
          {"at":"-14d","area":"notifications","added":5},
          {"at":"-13d","area":"search","reopened":2,"added":4},
          {"at":"-10d","area":"checkout","reopened":3,"added":8},
          {"at":"-8d","area":"billing","reopened":2,"added":6},
          {"at":"-7d","area":"search","added":5},
          {"at":"-3d","area":"checkout","reopened":4,"added":5},
          {"at":"-2d","area":"search","added":4},
          {"at":"-1d","area":"notifications","added":3},
          {"at":"-6h","area":"checkout","reopened":3}
        ]</script>
        <script type="application/json" data-areas>[
          {"id":"checkout","label":"Checkout"},
          {"id":"search","label":"Search"},
          {"id":"billing","label":"Billing"},
          {"id":"notifications","label":"Notifications"}
        ]</script>
      </mv-net-progress>
    </section>

    <!-- A codebase, from pre-computed analytics: lines added vs still there after 21 days -->
    <section class="npd-col" aria-labelledby="npd-code-h">
      <div class="npd-bar"><div><h3 id="npd-code-h">checkout-service repository</h3><p>Compact · pre-computed periods · lines still there after 21 days</p></div></div>
      <mv-net-progress variant="compact" label="Lines of code" unit="line|lines" gross-label="Added" net-label="Still there" net-verb="are still there">
        <script type="application/json" data-periods>[
          {"start":"-52d","gross":3120,"net":1480},
          {"start":"-45d","gross":2840,"net":1620},
          {"start":"-38d","gross":3400,"net":2050},
          {"start":"-31d","gross":2600,"net":1790},
          {"start":"-24d","gross":2950,"net":2240},
          {"start":"-17d","gross":2210,"net":1810,"state":"settling"},
          {"start":"-10d","gross":1980,"net":1700,"state":"settling"},
          {"start":"-3d","gross":1460,"net":1350}
        ]</script>
      </mv-net-progress>
    </section>
  </div>

  <script type="module">
    const spec = document.getElementById("npd-spec");
    await customElements.whenDefined("mv-net-progress");
    const $ = (id) => document.getElementById(id);
    const log = (name, text) => { $("npd-event").textContent = name; $("npd-last").textContent = text; };

    spec.addEventListener("mv-period-change", (e) => log("mv-period-change", `${e.detail.period.name}: ${e.detail.period.gross.toLocaleString("en-US")} written, ${e.detail.period.net.toLocaleString("en-US")} kept`));
    spec.addEventListener("mv-churn-select", (e) => {
      const d = e.detail;
      if (d.action === "open") {
        e.preventDefault(); // the app would scroll to or open that section
        log("mv-churn-select", `Open “${d.label}” (${d.changedBack} changes back, ${Math.round(d.ratio * 100)}% taken back)`);
      } else log("mv-churn-select", d.id ? `Filter to “${d.label}”` : "Show all areas");
    });

    $("npd-period").addEventListener("mv-change", (e) => {
      const day = e.detail.value === "day";
      spec.limit = day ? 14 : 8;
      spec.period = day ? "day" : "week";
    });
    $("npd-expect").addEventListener("change", (e) => { spec.expect = e.target.checked ? "converge" : null; });

    let next = "$59/mo";
    $("npd-revert").addEventListener("click", (e) => {
      spec.addEvents({ at: new Date(), area: "pricing", removed: 300, added: 300, version: next });
      log("addEvents", `Pricing & plans changed back to ${next}`);
      next = next === "$59/mo" ? "$49/mo" : "$59/mo";
      e.currentTarget.textContent = `Change pricing back to ${next}`;
    });
  </script>
</div>

API

Attributes

Name유형DefaultDescription
labelstringName of what is tracked ("Checkout 2.0 launch spec"). Titles the group, the data table caption and the header.
unit"singular|plural" | stringchange|changesWhat is counted: "word|words", "line|lines", "ticket|tickets", "clause|clauses". A single word gets an automatic plural (a trailing s is read as already plural).
gross-labelstringWrittenLegend and column name of gross activity ("Added", "Closed", "Proposed"). Its lowercase form is the default gross-verb.
net-labelstringKeptLegend and column name of net progress ("Still there", "Stayed closed", "Agreed").
gross-verbstringlowercase gross-labelVerb in sentences: “For every 10 words {gross-verb} this week…”.
net-verbstringremainVerb in sentences: “…, 3 {net-verb}.” ("are still there", "stayed closed").
periodday | week | monthweekBucket size for events, and the unit named in sentences, the trend and the table. Pre-computed periods keep their own starts.
limitnumber8How many periods are shown, ending with the one that contains now. Older events still count: their work can be taken back inside the range.
windowtime ("14d", "2w", "36h", days as a number)A take-back only counts as churn if it undoes work younger than this ("lines still there after 14 days"); older work has settled and its later revisions are reported separately as “earlier work revised”. Periods closer to now than the window are marked as still settling. Without it, any later take-back counts and only the current period is marked in progress.
topnumber5Length of the “Most rework” list (areas with take-backs or back and forth, by amount taken back).
oscillationnumber2How many changes back an area needs before it is listed under “Back and forth”.
trendnumber4How many recent periods with activity the exploring/converging trend is fitted on (the current period is left out once there are at least four).
expectexplore | convergeWhat the plan expects now. With converge, an exploring phase adds a neutral pointer to the areas that rework most; with explore, it is confirmed as matching the plan.
nowdate (ISO, YYYY-MM-DD, epoch ms)the current timeReference time for the range, relative dates and the in-progress / settling states (reports, tests, time travel).
week-start0-61 (Monday)First day of a week bucket (0 = Sunday).
localeBCP 47 tagen-USNumber, percent and date formatting.
variantfull | compactfullcompact keeps the header, the sentence, the figures, the chart and the data table, and hides the trend, back and forth and area panels (dashboard tiles).
areastring (area id)Filters the chart, sentence, trend and table to one area. Reflected when the user picks an area; remove it to show all areas.

Properties

Name유형Description
eventsArray<{ at, area?, added?, removed?, reverted?, reopened?, state?, version?, href? }>Timestamped edits (also read from a child <script type="application/json" data-events>). at: Date, epoch ms, ISO, YYYY-MM-DD or relative ("-3d", "-2w", "-6h", "now"). added is new work; removed, reverted and reopened are take-backs charged to the most recent work of the same area. state (or version, a readable label such as "$49/mo") fingerprints the area after the edit: returning to an earlier state counts as a change back; a reverted amount without state counts as one too. Tuples [at, area, added, removed, state] are accepted.
periodsArray<{ start | label, gross, net | taken, state? }> | Array<[start, gross, net]>Pre-computed periods (also data-periods), used when there are no events. net may be negative (more removed than added). state: "current" | "settling" | "final"; otherwise the period containing now is current.
areasArray<{ id, label, href?, gross?, net?, changedBack?, versions?, periods? }> | Record<id, …>Area names and jump links (also data-areas). Without events it also carries the area figures; an area with its own periods series can filter the chart.
summaryobjectRead-only: { mode, area, periods (gross, net, taken, ratio, earlier, state, name…), selected, totals, phase { phase, kind, from, to, avg, n }, areas, oscillations }.
selectedPeriodnumberIndex of the selected period in the visible range (latest by default; stays on the latest when new periods arrive).
selectedAreastring | nullMirror of the area attribute.
stringsPartial<Record<string, string>>Overrides for every visible text and announcement (headline templates ratio / all / nearlyAll / none / negative, when-phrases, trend texts, back / backTwo, titles, table headers…). English defaults.

Methods

NameDescription
selectPeriod(index | "latest")Selects a period; emits mv-period-change. Returns false when out of range or canceled.
selectArea(id | null)Filters to an area or back to all; emits mv-churn-select. Returns false when unknown or canceled.
addEvents(events)Appends live events (one or an array) and recomputes; selections are kept.
refresh()Recomputes, e.g. after mutating the events array in place or when now should move.

Events

NameDescription
mv-period-changeCancelable. A period was selected (click, arrow keys, API). detail: { index, period (gross, net, taken, ratio, earlier, state, name, start, end), previous, via: "pointer" | "keyboard" | "api" }. preventDefault() keeps the current selection.
mv-churn-selectCancelable. An area was picked. detail: { id, label, href, action: "filter" | "open" | "clear", gross, net, taken, ratio, changedBack, trail, via }. For filter, preventDefault() keeps the chart as it is; for open (the jump link), preventDefault() stops the navigation so the app can route or scroll itself.

Content structure

NameDescription
script[data-events] / script[data-periods] / script[data-areas]Optional JSON children read once on first connection (properties set before that win).

CSS classes

NameDescription
mv-net-progress-rootThe card (role=group, data-variant, data-mode="events | periods").
mv-net-progress-head / -label / -range / -chipHeader: label, date range with the overall kept share, phase chip (data-phase="exploring | converging | steady").
mv-net-progress-headline / -pips / -stats / -noteThe sentence for the selected period (numbers in <b>), ten pips filled with the kept share, the three figures, and the earlier-work note.
mv-net-progress-plot / -col / -bar / -taken / -tag / -tickChart: one radio button per period (data-selected, data-state="current | settling | final", data-empty, --_g, --_n, --_t), paired bars (data-kind="gross | net"), hatched taken-back part, kept % tag, period tick.
mv-net-progress-trend / -phase / -sparkExploring or converging panel: phase badge, sentence, churn sparkline with the fitted trend dashed.
mv-net-progress-back / -back-item / -trail / -verBack and forth panel: one filter button per oscillating area with its version trail.
mv-net-progress-areas / -area / -area-btn / -area-bar / -area-linkMost rework list: filter toggle (aria-pressed), kept vs taken-back bar, jump link.
mv-net-progress-data / -table<details> disclosure with the full data table.

CSS variables

NameDefaultDescription
--mv-net-progress-netvar(--mv-accent)Kept (net progress) bars, pips and area bars.
--mv-net-progress-grossmuted neutralGross activity bars.
--mv-net-progress-takenvar(--mv-fg-muted)Ink of the taken-back hatching and of the churn sparkline.
--mv-net-progress-exploringvar(--mv-info)Tint of the Exploring phase badge.
--mv-net-progress-convergingvar(--mv-success)Tint of the Converging phase badge.
--mv-net-progress-chart-height11remHeight of the bar area.
--mv-net-progress-paddingvar(--mv-space-5)Card padding.

Accessibility

The card is a group named by its label and described by the headline, so the key fact is read in words first (“For every 10 words written this week, 3 remain so far.”); the ten pips, bars, area bars and sparkline are aria-hidden because the same facts are in text. The chart is a radiogroup of real buttons, one per period, each with a full label (“Week of Sep 14: 1,240 words written, 380 remain, 860 taken back (69% churn). Still settling: take-backs can still arrive.”); it is a single Tab stop, arrows, Home and End move and select, and the selection follows focus. Its complete equivalent is a real table (period, written, kept, taken back, churn, status) behind a native <details> disclosure that everyone can open. Nothing is color-only: taken-back work is hatched, still-settling periods are dashed, the phase is a word with an icon, and changes back are counted in words. Area filters and back-and-forth items are toggle buttons (aria-pressed); jump links are real links with a named target (“Open Pricing & plans”). A polite live region speaks only when the area filter changes (“Showing Pricing & plans only. For every 10 words…”); period moves are already read by the radio labels. All wording describes the work, never a person, and exploring is framed as expected rather than as a problem; no red is used. Reduced motion (OS setting or data-motion="reduce"): no draw-in, bars and pips jump instead of growing. Forced colors: kept uses Highlight, gross an outlined Canvas bar, taken back a CanvasText hatch, and the selected period gets a Highlight outline.

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