/*
Maintains packages/share/src/counter-utils.ts as an authored share authored module so agents can evolve it without rediscovering local boundaries.
- Do not depend on GSAP or DOM.
- Do not handle animation logic.
- RFC-1097: step 6 — compass.migrate codemod run
Mechanical v1 to v2 header migration across the workspace: 942 files rewritten — CHANGE_SUMMARY windows collapsed into history, forbidden v1 blocks stripped, KEY_DECISIONS seeded from @ai-invariant comments (5 files) or TODO placeholders (103 files), blocks reordered to canonical order.
- RFC-1097: sweep — werkstatt-engine clean
Sweep batch 4: 73 Compass headers on headerless engine files (certification, component-runtime, isolation, evolution, testing), real KEY_DECISIONS on 75 files (kernel, cache, dht, swim, gitmesh, runtime), ~80 purpose expansions (CONTRACT-02/PURPOSE-02), non-goals on 13 CONTRACT-03 files, CS-07 history literal fix repo-wide (253 files). Policy: .template.ts/.template.astro excludedPaths. werkstatt-engine now 0 diagnostics.
*/
export interface CounterStat {
value?: string;
label: string;
numericValue?: number;
prefix?: string;
suffix?: string;
decimals?: number;
duration?: number;
}
export interface ResolvedCounterStat extends CounterStat {
isAnimated: boolean;
startValue: string;
finalFormatted: string;
}
export function formatNumber(value: number, locale: string, decimals = 0): string {
return new Intl.NumberFormat(locale, {
minimumFractionDigits: decimals,
maximumFractionDigits: decimals,
}).format(value);
}
export function parseNumeric(val: string | undefined): number | undefined {
if (!val) return undefined;
const cleaned = val.replace(/[^\d.,]/g, "").replace(/,/g, "");
const num = parseFloat(cleaned);
return isNaN(num) ? undefined : num;
}
// Create start value with same width as final, starting from "1" in highest digit
export function getStartValue(formattedFinal: string): string {
// Replace digits: first digit becomes "1", rest become "0"
// "1,250" → "1,000", "14" → "10"
let firstDigit = true;
return formattedFinal.replace(/\d/g, () => {
if (firstDigit) {
firstDigit = false;
return "1";
}
return "0";
});
}
export function resolveCounterStats(
stats: CounterStat[],
animated: boolean | undefined,
locale: string,
): ResolvedCounterStat[] {
return stats.map((stat) => {
const numericValue = stat.numericValue ?? parseNumeric(stat.value);
const isAnimated: boolean = (animated ?? false) && numericValue !== undefined;
const decimals = stat.decimals ?? 0;
const finalFormatted =
numericValue !== undefined
? formatNumber(numericValue, locale, decimals)
: (stat.value ?? "");
const startValue = isAnimated ? getStartValue(finalFormatted) : finalFormatted;
return { ...stat, numericValue, isAnimated, startValue, finalFormatted };
});
}