codeburn/app/renderer/lib/format.ts
iamtoruk a02ae15b6b feat(app): rebuild Sessions — fixed columns, clean paths, inline expand
- share a shortenProjectPath helper (Sankey + Sessions) so rows show
  "projects/eywa" instead of the dash-mangled full path
- fixed-column table layout so values align and stop shifting row-to-row
- TUI-style inline row expansion: click a row to unfold its 8-stat detail in
  place (one at a time, chevron + aria-expanded); replaces the navigate-away view

typecheck clean; 128/128 tests pass.
2026-07-12 13:38:45 -07:00

62 lines
2.5 KiB
TypeScript

export function formatUsd(n: number): string {
return `$${n.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`
}
/** Shorten filesystem and CLI-mangled project paths to their useful trailing segments. */
export function shortenProjectPath(value: string, maxSegments = 3): string {
const trimmed = value.trim()
const pathDelimited = /[\\/]/.test(trimmed)
const parts = trimmed.split(pathDelimited ? /[\\/]+/ : /-+/).filter(Boolean)
let displayParts = parts.slice(-Math.max(1, maxSegments))
// A tail rooted directly under a home directory starts with the user name,
// which adds noise rather than useful project context.
const precedingPart = parts.at(-(displayParts.length + 1))
if (displayParts.length > 1 && /^(users?|home)$/i.test(precedingPart ?? '')) {
displayParts = displayParts.slice(1)
}
if (/^(projects|src|config)$/i.test(displayParts[0] ?? '')) {
displayParts[0] = displayParts[0].toLowerCase()
}
return displayParts.join('/') || trimmed
}
/** Compact token/count formatting: 1_842 → "1.8K", 184_000 → "184K", 1_200_000 → "1.2M". */
export function formatCompact(n: number): string {
if (!Number.isFinite(n)) return '—'
if (n === 0) return '0'
const abs = Math.abs(n)
if (abs < 1_000) return String(Math.round(n))
if (abs < 1_000_000) return `${trim(n / 1_000)}K`
if (abs < 1_000_000_000) return `${trim(n / 1_000_000)}M`
return `${trim(n / 1_000_000_000)}B`
}
// One decimal, but drop a trailing ".0" (184.0K → "184K", 1.2K stays "1.2K").
function trim(v: number): string {
const s = v.toFixed(1)
return s.endsWith('.0') ? s.slice(0, -2) : s
}
/** "Jul 10" — short month + day, no year. */
export function formatDayShort(iso: string): string {
const d = new Date(iso)
return Number.isNaN(d.getTime()) ? '—' : d.toLocaleDateString('en-US', { month: 'short', day: 'numeric' })
}
/** "Jul 10, 2026" — full date. */
export function formatDayLong(iso: string): string {
const d = new Date(iso)
return Number.isNaN(d.getTime()) ? '—' : d.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })
}
/** "2h 14m" / "47m" / "38s" from a duration in ms. */
export function formatDuration(ms: number): string {
if (!Number.isFinite(ms) || ms <= 0) return '—'
const totalMin = Math.round(ms / 60_000)
if (totalMin < 1) return `${Math.round(ms / 1000)}s`
if (totalMin < 60) return `${totalMin}m`
return `${Math.floor(totalMin / 60)}h ${totalMin % 60}m`
}