refactor(format): one byte formatter for every surface

The document vault and the admin area each carried a formatBytes; the
same backup read 1.25 MB in one and 1.2 MB in the other, and only the
vault knew gigabytes. The vault rule (one decimal below 100, up to GB,
digits in the reader's locale) now lives in src/lib/format/bytes.ts and
takes the locale, so the server-rendered share page uses it too. A test
holds the definition to that one module.
This commit is contained in:
Marc-André Bombeck committed 2026-10-20 12:55:06 +02:00
1 parent 67cc56f707
commit 56daa57350
12 files changed
+84 -70

No files matched your search

@@ -11,7 +11,6 @@ import {
documentCardKeyAction,
documentDateKey,
expandRangeSelection,
formatBytes,
formatMonthLabel,
hasProcessingDocument,
isDocumentProcessing,
@@ -216,15 +215,6 @@ describe("timeline items", () => {
});
});
describe("byte formatting", () => {
it("scales units and keeps one decimal below 10", () => {
expect(formatBytes(0, "en")).toBe("0 B");
expect(formatBytes(512, "en")).toBe("512 B");
expect(formatBytes(26_214_400, "en")).toBe("25 MB");
expect(formatBytes(1_288_490_189, "en")).toBe("1.2 GB");
});
});
describe("upload response contract (§3.2)", () => {
it("HTTP 200 + meta.duplicate returns the EXISTING row as a duplicate", () => {
const body = JSON.stringify({
+2 -5
View File
@@ -31,11 +31,8 @@ import { cn } from "@/lib/utils";
import type { InboundDocumentDto } from "@/lib/validations/inbound-documents";
import { DOCUMENT_KIND_ICONS } from "./document-kind-meta";
import type { UploadQueueItem } from "./use-document-upload";
import {
documentCardKeyAction,
formatBytes,
isDocumentProcessing,
} from "./vault-utils";
import { formatBytes } from "@/lib/format/bytes";
import { documentCardKeyAction, isDocumentProcessing } from "./vault-utils";
/** How long the "Ready" chip stays up after a card's index lands. */
const READY_FLASH_MS = 3000;
@@ -102,7 +102,7 @@ import {
useDocumentSummary,
useSuggestDetails,
} from "./use-document-assist";
import { formatBytes } from "./vault-utils";
import { formatBytes } from "@/lib/format/bytes";
import { QueryErrorRow } from "@/components/ui/query-error-row";
type PatchInput = {
@@ -75,7 +75,7 @@ import type { InboundDocumentKindValue } from "@/lib/validations/inbound-documen
import { WrittenOutcomeLine } from "@/components/outcome/written-outcome-line";
import type { WrittenOutcome } from "@/lib/outcome/written-outcome";
import { formatBytes } from "../vault-utils";
import { formatBytes } from "@/lib/format/bytes";
import {
errorCodeOf,
isStoppingError,
+1 -1
View File
@@ -19,7 +19,7 @@ import { type RefObject } from "react";
import { Progress } from "@/components/ui/progress";
import { useTranslations } from "@/lib/i18n/context";
import type { DocumentUsageDto } from "@/lib/validations/inbound-documents";
import { formatBytes } from "./vault-utils";
import { formatBytes } from "@/lib/format/bytes";
/** Usage fraction above which the quota bar appears. */
const QUOTA_BAR_THRESHOLD = 0.8;
-15
View File
@@ -302,21 +302,6 @@ export function formatMonthLabel(key: string, locale: string): string {
}).format(new Date(`${key}-15T12:00:00.000Z`));
}
/** Locale byte formatting: 0 B / 412 KB / 1.2 MB / 2.4 GB. */
export function formatBytes(bytes: number, locale: string): string {
const units = ["B", "KB", "MB", "GB"] as const;
let value = Math.max(0, bytes);
let unit = 0;
while (value >= 1024 && unit < units.length - 1) {
value /= 1024;
unit += 1;
}
const digits = unit === 0 || value >= 100 ? 0 : value >= 10 ? 1 : 1;
return `${new Intl.NumberFormat(locale, {
maximumFractionDigits: digits,
}).format(value)} ${units[unit]}`;
}
/**
* Shift-click range selection over the timeline's visual order. Selects
* every id between the last plainly-toggled anchor and the target