/** * When to use a **range slider** vs text contains for column filters. * * | Use `range` | When | * |-------------|------| * | `cellKind: "progress"` | Fixed bounded domain (0–100 %) | * | `cellKind: "currency"` / `"numeric"` | Numeric column — min/max from dataset (`dataBounds`) unless `rangeMin`/`rangeMax` set | * | Explicit `filter.type: "range"` | Hub declares numeric band filter | * * Override back to text with `filter: { type: "text" }` when free-form match is intended. */ export function initialsFromDisplayName(name: string): string { return name .split(/\s+/) .filter(Boolean) .slice(0, 2) .map(w => w[0]?.toUpperCase() ?? "") .join("") } export function computeNumericColumnBounds>( rows: T[], key: string, ): { min: number; max: number } { let min = Infinity let max = -Infinity for (const row of rows) { const n = Number(row[key]) if (!Number.isFinite(n)) continue min = Math.min(min, n) max = Math.max(max, n) } if (!Number.isFinite(min) || !Number.isFinite(max)) { return { min: 0, max: 100 } } if (min === max) { return { min, max: max + 1 } } return { min, max } } /** Step size for currency sliders — finer when values are small. */ export function defaultRangeStep(min: number, max: number, unit?: string): number { if (unit === "%") return 1 const span = max - min if (span <= 10) return 0.01 if (span <= 100) return 1 if (span <= 1000) return 5 return 10 }