/** * Storage Monitor — library pane footer (spec §02 → Storage Monitor; * docs/architecture/10 → "Resolved — storage composition indicator"). * * Shows what local storage is *made of* — Snippets, Datasets, and App (the * precached shell + overhead) — as a proportional bar plus a labelled legend. * It is **not** a "used of quota" gauge: the browser's `quota` is an unreliable * padded approximation, so we show real measured sizes instead and let the genuine * out-of-room moment surface where it happens (a save-failure toast). * * Accessibility (council → APG meter / NN/g / WCAG 1.4.1): the bar is **decorative** * (`aria-hidden`) and carries no role="meter" — a meter needs a meaningful maximum, * which we don't have. The **legend is the source of truth**: real text labels + * sizes that assistive tech reads, so meaning never rests on colour alone. * * Snippet/dataset bytes are measured from our own stores (always available); the * App segment needs the origin estimate, so it appears only when that resolves. */ import { useEffect, useMemo, useState } from 'react'; import { humanizeBytes, jsonByteSize, STORAGE_MONITOR_MIN_USER_BYTES, summarizeStorage, } from '@core/storage-estimate'; import { readOriginUsage } from '../infrastructure/storage-estimate'; import { useSnippetStore } from '../stores/SnippetStore'; import { useDatasetStore } from '../stores/DatasetStore'; import styles from './StorageMonitor.module.css'; /** Segment key → its colour class (shared by the bar slice and the legend swatch). */ const SEGMENT_CLASS = { snippets: styles.segSnippets, datasets: styles.segDatasets, app: styles.segApp, } as const; export default function StorageMonitor() { const snippets = useSnippetStore((s) => s.snippets); const datasets = useDatasetStore((s) => s.datasets); // Measure our own data synchronously. Snippets are serialized; datasets already // carry a byte `size`. Memoized so we only re-measure when the lists change. const snippetBytes = useMemo( () => snippets.reduce((n, snip) => n + jsonByteSize(snip), 0), [snippets], ); const datasetBytes = useMemo(() => datasets.reduce((n, d) => n + (d.size ?? 0), 0), [datasets]); // Stay hidden until the user's own data is worth a glance — below the floor it's // ambient noise, and the app shell would dominate the bar anyway (spec §02). const belowFloor = snippetBytes + datasetBytes < STORAGE_MONITOR_MIN_USER_BYTES; // The origin total is async + optional. Re-read after our data changes, since a // save moves usage; undefined when the Storage Manager API is unavailable. const [usageBytes, setUsageBytes] = useState(undefined); useEffect(() => { let cancelled = false; void readOriginUsage().then((u) => { if (!cancelled) setUsageBytes(u); }); return () => { cancelled = true; }; }, [snippetBytes, datasetBytes]); const composition = useMemo( () => summarizeStorage({ usageBytes, snippetBytes, datasetBytes }), [usageBytes, snippetBytes, datasetBytes], ); // Hidden below the user-data floor, or with nothing measured yet (truly empty). if (belowFloor || composition.totalBytes <= 0) return null; const { segments, totalBytes } = composition; return (
{/* Decorative composition bar — the legend below is the accessible source. */} ); }