Vanilla Breeze

iron-triangle

Project-shape constraint surface — captures Time × Cost × Scope on one form and computes a single integer capacityPoints budget that downstream Planning Pack components (notably ) can spend on quality decisions.

Overview

The <iron-triangle> component is the Planning Pack's constraint surface. It captures Time, Cost, and Scope as one decision and computes a single integer capacityPoints budget that downstream tools — first and foremost <nfr-compass> — spend against quality choices. Reach for it whenever a project's shape is currently scattered across targetLaunch fields, MoSCoW spreadsheets, and informal "team-size" guesswork; one surface, one decision moment, one number the rest of the planning artifacts can read.

Pre-upgrade the markup is a plain <form> with three <fieldset>s, so a server can persist values and even compute capacity without the bundle. After upgrade the component computes capacity live, exposes a stable hash for drift detection, and records every revision with a required reason.

Live demo

Edit any input — the capacity readout and formula text below the inputs update in real time.

The deformable triangle

The component renders an inline SVG triangle into the [data-iron-triangle-viz] figure. Each vertex sits on an equilateral baseline ray and stretches outward by a factor in [0.55, 1.45] based on its constraint's relative magnitude (time = total weeks, cost = FTE × hours/week, scope = weighted feature counts). A balanced project stays close to equilateral; a long-deadline project pushes the TIME vertex up; a tiny team pulls the COST vertex toward center.

The capacity number anchors at the centroid (origin), so the integer never moves when the shape changes.

The capacity formula

Capacity is one integer the team picked together. The default formula is intentionally tiny:

The focus factor represents the share of FTE actually available for engineering quality work — the remaining 40% goes to feature delivery, meetings, ops, and the unknowns. 0.6 is the middle of the 50–70% range most teams settle on; tune per project with data-focus-factor.

ProjectMathcapacityPoints
Solo, 6-week project 6 wk × 1 FTE × 0.6 4
Small team, quarter 12 wk × 3 FTE × 0.6 22
Full team, half-year 26 wk × 5 FTE × 0.6 78

Manual override

Sometimes the budget already exists — a board commitment, an OKR, or a number carried over from a prior project. Flip to manual mode and the integer becomes editable directly; the T/C/S inputs stay visible and saved (they document why the project looks the way it does) but no longer drive the readout. capacitySource records which path produced the saved number.

If you'd rather not wire it manually, point the compass at the triangle by id and the binding happens for you:

Revisions with reasons

Edits made via the revise() method append to revisionLog and emit iron-triangle:revise. The reason is required (≥ 10 characters) so the constraint surface accumulates institutional memory rather than vanishing into Slack threads.

Attributes

AttributeTypeDefaultDescription
name string triangle Form-association field name.
data-focus-factor number (0–1) 0.6 Multiplier in the default capacity formula — share of FTE available for engineering quality work.
data-min-capacity integer 1 Floor for capacityPoints. Below this, :state(unbudgeted) is set.
data-capacity-formula string Custom formula expression (extension point; out of scope for v1).
disabled boolean absent All inputs disabled.
locked boolean absent Read-only mode for shipped vectors.

Slots

SlotPurposeRequired
title Heading text shown above the form no (defaults to "Iron Triangle")
time-controls Override the default Time inputs no
cost-controls Override the default Cost inputs no
scope-controls Override the default Scope inputs no
capacity-readout Override the readout block (advanced) no
footer Save / Submit button area no

Per VB convention, content lives in slots and state lives in attributes. The default-slot fieldsets named time, cost, scope, and capacity are the markup contract — you can hand-author them (as in the static fallback) or let the component inject sensible defaults if absent.

Events

EventDetailWhen
iron-triangle:change { time, cost, scope, capacityPoints, capacitySource, hash, source } Any input or property change. source is 'pointer', 'keyboard', or 'api'.
iron-triangle:revise { field, from, to, reason } A revision is committed via revise().
iron-triangle:mode { from, to } Capacity source flips between 'formula' and 'manual'.

Internal state hooks

Iron-triangle exposes CustomStateSet entries for CSS targeting via :state().

StateWhen
:state(formula) Capacity source is the formula (default).
:state(manual) Capacity source is a manual integer.
:state(over-deadline) The Time-fieldset deadline date is in the past (passive warning).
:state(unbudgeted) Computed capacityPoints < data-min-capacity.

JavaScript API

Property / MethodTypeDescription
.time { sprintWeeks, sprintCount, hoursPerWeek, deadline } Get / set the Time corner.
.cost { teamFTE, budgetTier, contractorBudget } Get / set the Cost corner.
.scope { mustHaveCount, shouldHaveCount, scopeNotes } Get / set the Scope corner.
.capacityPoints integer Current budget. Read-only when capacitySource === 'formula'; writable in manual mode.
.capacitySource 'formula' \| 'manual' Which mode produced the saved number.
.hash string (readonly) FNV-1a of the T/C/S object — for drift detection.
.revisionLog array (readonly) Append-only edit history.
.value object (readonly) Full snapshot for serialization.
.revise(field, newValue, reason) method Programmatic edit; throws if reason.length < 10.
.setManual(integer) method Switch to manual mode and set capacityPoints.
.setFormula(formulaString?) method Switch back to formula mode (default formula or custom).
.recalc() method Force capacity recomputation (rarely needed).

CSS Tokens

TokenDefaultPurpose
--iron-triangle-padding var(--size-l) Outer padding
--iron-triangle-section-gap var(--size-l) Vertical gap between Time / Cost / Scope
--iron-triangle-input-min 8rem Min width per numeric input
--iron-triangle-readout-bg var(--color-surface-raised)Capacity readout background
--iron-triangle-readout-size var(--font-size-3xl) Capacity number font size
--iron-triangle-formula-color var(--color-text-muted) Formula explanation text
--iron-triangle-warning-color var(--color-warning) Over-deadline / unbudgeted warning

Static fallback

Without JavaScript, the form still works — three <fieldset>s of native inputs that submit raw T/C/S values to the form's action. The server can persist the values and even compute capacity. The component just adds the live readout, formula visualization, and revision-tracking machinery on upgrade.

Accessibility

  • Native form semantics throughout. Real <fieldset> / <legend> grouping; the capacity readout is an <output> with aria-live="polite".
  • Mode toggle is a real button. The formula / manual switch uses <button aria-pressed>, not a checkbox-and-label hack.
  • Warnings are never color-only. The deadline-in-past warning includes the literal text "Deadline has passed" alongside any styling.
  • Logical properties throughout for clean RTL and writing-mode support.

Related

  • <nfr-compass> — primary consumer; spends capacityPoints on quality picks and stamps the triangle's hash for drift detection.
  • <adr-wc> — record the Iron Triangle decision itself as an ADR.
  • UX Planning Pack — loads iron-triangle, nfr-compass, and the rest of the planning surface together.