mirror of
https://github.com/olehomelchenko/astrolabe.git
synced 2026-08-08 10:12:34 +00:00
94 lines
3.4 KiB
TypeScript
94 lines
3.4 KiB
TypeScript
/**
|
|
* Storage Monitor — library pane footer (spec §02 → Storage Monitor;
|
|
* spec §10 → "Warn before storage failure"; docs/architecture/10 §1 status
|
|
* indicator channel).
|
|
*
|
|
* Fetches the browser storage estimate on mount (readStorageEstimate), runs it
|
|
* through summarizeStorage, and renders:
|
|
* - human-readable "used of quota" text
|
|
* - a fill bar (meter) reflecting the percentage used
|
|
* - escalating visual treatment at warning / critical levels via design tokens
|
|
*
|
|
* When the Storage Manager API is unavailable (feature-detected by the adapter)
|
|
* the component renders nothing — a missing ambient indicator is harmless, and
|
|
* surfacing a "unavailable" line adds noise with no actionable value.
|
|
*
|
|
* Critical state is announced politely via `aria-live="polite"` so assistive
|
|
* technology is informed without interrupting the user mid-task (arch 10 §5 —
|
|
* status indicators are ambient, not assertive).
|
|
*/
|
|
|
|
import { useEffect, useState } from 'react';
|
|
import { humanizeBytes, type StorageSummary } from '@core/storage-estimate';
|
|
import { readStorageEstimate } from '../infrastructure/storage-estimate';
|
|
import styles from './StorageMonitor.module.css';
|
|
|
|
/** Level-to-label for the accessible announcement copy. */
|
|
const LEVEL_LABEL: Record<StorageSummary['level'], string> = {
|
|
ok: '',
|
|
warning: 'Storage is getting full.',
|
|
critical: 'Storage is almost full. Delete snippets to free space.',
|
|
};
|
|
|
|
export default function StorageMonitor() {
|
|
const [summary, setSummary] = useState<StorageSummary | null>(null);
|
|
|
|
useEffect(() => {
|
|
let cancelled = false;
|
|
void readStorageEstimate().then((s) => {
|
|
if (!cancelled) setSummary(s);
|
|
});
|
|
return () => {
|
|
cancelled = true;
|
|
};
|
|
}, []);
|
|
|
|
// While loading, or when the API is unavailable, render nothing.
|
|
if (!summary || !summary.available) return null;
|
|
|
|
const usedText = humanizeBytes(summary.usedBytes);
|
|
const quotaText = humanizeBytes(summary.quotaBytes);
|
|
const pct = Math.round(summary.fraction * 100);
|
|
const announcement = LEVEL_LABEL[summary.level];
|
|
|
|
return (
|
|
<div className={`${styles.monitor} ${styles[summary.level]}`} aria-label="Storage usage">
|
|
{/* Usage text */}
|
|
<div className={styles.label}>
|
|
<span className={styles.used}>{usedText}</span>
|
|
<span className={styles.separator}> of </span>
|
|
<span className={styles.quota}>{quotaText}</span>
|
|
</div>
|
|
|
|
{/*
|
|
* Fill bar — ARIA meter (WAI-ARIA 1.1).
|
|
* role="meter" conveys a scalar value within a known range; aria-valuetext
|
|
* gives a human-readable reading that matches the visible label.
|
|
*/}
|
|
<div
|
|
role="meter"
|
|
aria-label="Storage used"
|
|
aria-valuenow={pct}
|
|
aria-valuemin={0}
|
|
aria-valuemax={100}
|
|
aria-valuetext={`${usedText} of ${quotaText} (${pct}%)`}
|
|
className={styles.track}
|
|
>
|
|
<div className={styles.fill} style={{ width: `${pct}%` }} />
|
|
</div>
|
|
|
|
{/*
|
|
* Polite live region — announces the warning / critical state to assistive
|
|
* technology without interrupting ongoing work. Empty for the 'ok' level so
|
|
* there is no announcement when storage is healthy (arch 10 §1 — status
|
|
* indicators are ambient, not assertive; only escalation warrants notice).
|
|
*/}
|
|
{announcement && (
|
|
<p role="status" aria-live="polite" className={styles.announcement}>
|
|
{announcement}
|
|
</p>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|