KQ-1773 · SPEND/GUARD
Spend Guard
A budget-alerts panel for a project's cloud spend this month: one bar carries the spend as a fill, the forecast to month end as a hatch, a hairline at the budget and a marker per alert threshold with its recipients' avatars riding on its head, while a month strip below pins each threshold to the day it fired or will fire at the current pace, joined by a leader. Dragging a marker follows the pointer 1:1 and rubber-bands past the axis, the avatars follow on springs.snap and the date pin trails on springs.glide, and release snaps to a whole percent on snap with the throw's velocity; crossing what is already spent flips the head on springs.flick. Every marker is a slider (arrows one percent, Shift five, Page keys ten, Home and End), the recipients menu is a real menu, and an optional hard limit says plainly what stops.
Waylight · cloud spend
October 2026 · budget $5,000
3 alerts · budget $5,000 · stops at $6,250
Install
One command — the source lands in your repo. Or copy it from the Code tab.
$ pnpm dlx shadcn@latest add @kinetiq/spend-guard
Props
- markerStyle
- "pin" | "flag" | "tick"
- default "pin"
- How a threshold marker is drawn: a round head carrying the avatars, a flag off the stem, or a hairline tick.
- barHeight
- number
- default 20
- Thickness of the spend bar in px; markers and the month strip scale with it.
- palette
- "cobalt" | "ember" | "moss" | "mono"
- default "cobalt"
- Tone of the spend fill, forecast hatch and markers; the hard limit keeps its danger tone.
- budget
- number
- default 5000
- The monthly budget, in the currency format prints.
- thresholds / defaultThresholds
- SpendThreshold[]
- default — / defaultSpendThresholds
- The alert thresholds, controlled or initial: { id, percent (of budget), recipients (ids) }.
- onThresholdChange
- (thresholds: SpendThreshold[], id: string) => void
- default —
- Fires from the drag, key, add, remove or recipient change that changed the thresholds, with all of them and the id that changed.
- recipients
- SpendRecipient[]
- default defaultSpendRecipients
- Everyone who can be alerted: { id, name, role?, hue? }.
- onRecipientsChange
- (thresholdId: string, recipientIds: string[]) => void
- default —
- A threshold's recipients changed, with the new ids.
- hardLimit / defaultHardLimit
- number | null
- default — / null
- The amount at which spending stops, controlled or initial; null for none.
- onHardLimitChange
- (amount: number | null) => void
- default —
- The hard limit was switched on, moved or switched off.
- hardLimitEffect
- string
- default "New deployments pause and preview environments scale to zero. Production keeps serving, and nothing is deleted."
- One plain sentence: what stops at the hard limit and what keeps running.
- daily
- number[]
- default defaultSpendDaily
- Spend per day of the current month so far, day 1 first; the pace is the mean of the last seven.
- now
- number
- default defaultSpendNow (17 Oct 2026)
- The current time in ms. Dates are read in UTC; never read from the clock.
- format
- (amount: number) => string
- default US dollars, no cents
- Formats every amount.
- onSave
- (config: SpendConfig) => void | Promise<void>
- default —
- Save was pressed with { thresholds, hardLimit }; return a promise to show Saving… and its outcome.
- status
- "ready" | "loading" | "empty" | "error"
- default "ready"
- Whether the spend data has arrived. The bar and its markers stay in every state.
- onRetry
- () => void
- default —
- Try again was pressed after the spend failed to load.
- project
- string
- default "Waylight"
- The project, in the heading.
- height
- number
- default 540
- Height of the surface in px; its body scrolls inside.
- disabled
- boolean
- default false
- Nothing can be moved, added or saved.
- className
- string
- default —
- Classes for the outer surface.
| Prop | Type | Default | Description |
|---|---|---|---|
| markerStyle | "pin" | "flag" | "tick" | "pin" | How a threshold marker is drawn: a round head carrying the avatars, a flag off the stem, or a hairline tick. |
| barHeight | number | 20 | Thickness of the spend bar in px; markers and the month strip scale with it. |
| palette | "cobalt" | "ember" | "moss" | "mono" | "cobalt" | Tone of the spend fill, forecast hatch and markers; the hard limit keeps its danger tone. |
| budget | number | 5000 | The monthly budget, in the currency format prints. |
| thresholds / defaultThresholds | SpendThreshold[] | — / defaultSpendThresholds | The alert thresholds, controlled or initial: { id, percent (of budget), recipients (ids) }. |
| onThresholdChange | (thresholds: SpendThreshold[], id: string) => void | — | Fires from the drag, key, add, remove or recipient change that changed the thresholds, with all of them and the id that changed. |
| recipients | SpendRecipient[] | defaultSpendRecipients | Everyone who can be alerted: { id, name, role?, hue? }. |
| onRecipientsChange | (thresholdId: string, recipientIds: string[]) => void | — | A threshold's recipients changed, with the new ids. |
| hardLimit / defaultHardLimit | number | null | — / null | The amount at which spending stops, controlled or initial; null for none. |
| onHardLimitChange | (amount: number | null) => void | — | The hard limit was switched on, moved or switched off. |
| hardLimitEffect | string | "New deployments pause and preview environments scale to zero. Production keeps serving, and nothing is deleted." | One plain sentence: what stops at the hard limit and what keeps running. |
| daily | number[] | defaultSpendDaily | Spend per day of the current month so far, day 1 first; the pace is the mean of the last seven. |
| now | number | defaultSpendNow (17 Oct 2026) | The current time in ms. Dates are read in UTC; never read from the clock. |
| format | (amount: number) => string | US dollars, no cents | Formats every amount. |
| onSave | (config: SpendConfig) => void | Promise<void> | — | Save was pressed with { thresholds, hardLimit }; return a promise to show Saving… and its outcome. |
| status | "ready" | "loading" | "empty" | "error" | "ready" | Whether the spend data has arrived. The bar and its markers stay in every state. |
| onRetry | () => void | — | Try again was pressed after the spend failed to load. |
| project | string | "Waylight" | The project, in the heading. |
| height | number | 540 | Height of the surface in px; its body scrolls inside. |
| disabled | boolean | false | Nothing can be moved, added or saved. |
| className | string | — | Classes for the outer surface. |