Reloading without the view saved in the link usually fixes it.
+diff --git a/docs/printing.md b/docs/printing.md index aa6e492a2..e280dd345 100644 --- a/docs/printing.md +++ b/docs/printing.md @@ -149,3 +149,55 @@ after the tab's data has loaded; it excludes the initial dashboard load. The timeout is a bounded wait with an explicit retry path, not a guarantee that every device or connection finishes within 15 seconds. Safari native printing remains unverified, and Firefox page numbering still needs its native print setting. + +## Port to the interactive dashboard (27 September 2026) + +The shadcn/ui dashboard (#490) replaced the PNG galleries with interactive charts +drawn from each variant's JSON dataset, so the print feature was carried over +onto the new components rather than merged line by line. Its behaviour is +unchanged: the live tree is printed under the same transient print context, with +selections stated as text, every table row up to the 500-row cap, and provenance +on every page. What changed is how each piece is implemented: + +- **Readiness.** Charts are SVG drawn from JSON, so preparation no longer decodes + images. A chart still loading (its module or its dataset) carries + `data-print-pending`; a chart whose dataset failed carries `data-print-error` + and names itself ("Could not load chart data:
@@ -145,10 +184,16 @@ function OrgPanel({ org, manifest, macro }: { org: string; manifest: Manifest; m {/* A section that could not load leaves a named gap rather than blanking the tab — the rest of the page is still worth reading. */} {unavailable.length > 0 && ( -
- Could not load {unavailable.length === 1 ? 'this section' : 'these sections'}:{' '} - {unavailable.join(', ')}. Everything else on this tab is unaffected — reload to try again. -
+- Failed to load the dashboard data. This is usually temporary — try again in a moment. -
- -{message}
- This is usually temporary — try again in a moment.
+ +{message}
+ - Generated {stamp(manifest.generated_at)} UTC · every table filters and sorts · click a chart - to enlarge. -
- {/* The org filter is the outermost scope — everything below it is "this - org's view" — so it sits above the content tabs. */} - {orgs.length > 1 &&
+
+ {manifest.macro_summaries?.[activeMacro] ?? + 'Explore activity and insights across the Hiero ecosystem.'} +
+ {narrowHeader && dataAsOf && ( +- {manifest.macro_absent_notes?.[activeMacro] ?? - `No ${activeMacro} data for ${shownOrg}.`} -
+Loading…
-- Incomplete document: dashboard data is still loading. Close print preview and wait for - the dashboard to finish loading. -
-+ Incomplete document: dashboard data is still loading. Close print preview and + wait for the dashboard to finish loading. +
++ Data as of +
+ ); +} + +/** Mirrors STALE_AFTER in export/data_api.py: the weekly refresh plus 1.5 days of slack. */ +const STALE_AFTER_HOURS = 132; +const relative = new Intl.RelativeTimeFormat('en', { numeric: 'auto' }); + +/** How old the data is, in words: the words carry the stale state, never the dot alone. */ +export function FreshnessStatus({ dataAsOf, now }: { dataAsOf: string; now?: number }) { + // Read once: the age is a summary, not a ticking clock. + const [mounted] = useState(() => Date.now()); + const hours = Math.max(0, ((now ?? mounted) - Date.parse(dataAsOf)) / 3_600_000); + const stale = hours > STALE_AFTER_HOURS; + const days = Math.round(hours / 24); + const summary = stale + ? `${days} days old, refresh overdue` + : `Updated ${hours < 36 ? relative.format(-Math.round(hours), 'hour') : relative.format(-days, 'day')}`; + return ( ++ {summary} +
+{emphasized(note)}
} - {methodology && methodology.length > 0 && ( -- Could not load chart: {chart.title} ({active.label}). -
+Chart data is not available yet
++ This view will appear when its source dataset is published in the next analytics + refresh. +
+{active.note ?? chart.note}
+- Chart still loading: {chart.title} ({active.label}). Wait for it to load, then print - again. -
+ {/* An interactive chart names itself in its header; the placeholder does not. */} + {!active.interactive && ( +{section.description}
-{rows.length} rows
} + {!printing &&{rows.length} rows
} > ); } diff --git a/web/src/components/CsvDownloadButton.tsx b/web/src/components/CsvDownloadButton.tsx index 2cdfea9d5..5d50841df 100644 --- a/web/src/components/CsvDownloadButton.tsx +++ b/web/src/components/CsvDownloadButton.tsx @@ -1,28 +1,42 @@ -/** The "Download CSV" button — builds its payload lazily on click. */ +/** The "Download CSV" button — builds (or fetches) its payload lazily on click. */ +import { DownloadIcon } from 'lucide-react'; +import { Button } from '@/components/ui/button'; import type { Manifest } from '../api'; import { downloadCsv, type CsvExport } from '../csv'; +/** The button itself, for callers whose download is not a CsvExport (a chart's companion file). */ +export function DownloadButton({ + onClick, + compact = false, +}: { + onClick: () => void; + /** Icon-only below the enclosing `@container`'s `lg` width (a narrow chart card). */ + compact?: boolean; +}) { + return ( + + ); +} + export function CsvDownloadButton({ payload, provenance, + compact, }: { payload: () => CsvExport; provenance: Manifest['provenance']; + compact?: boolean; }) { - return ( - - ); + return- Filter: “{globalFilter}”. {rows.length} of {table.options.data.length} rows match; rows - outside this filter are not printed. + Filter: “{globalFilter}”. {count(rows.length)} of {count(total)} rows match; rows outside + this filter are not printed.
)} {printing && rows.length > PRINT_ROW_LIMIT && (- Showing {PRINT_ROW_LIMIT} of {rows.length} rows in the current order.{' '} - {rows.length - PRINT_ROW_LIMIT} rows are not printed. Download CSV from this table on the - dashboard for the complete selection. + Showing {count(PRINT_ROW_LIMIT)} of {count(rows.length)} rows in the current order.{' '} + {count(rows.length - PRINT_ROW_LIMIT)} rows are not printed. Download CSV from this table + on the dashboard for the complete selection.
)} - table.setGlobalFilter(event.target.value)} - /> -|
+
+
+ table.setGlobalFilter(event.target.value)}
+ className="h-8 bg-background pr-8 pl-8 text-xs"
+ />
+ {globalFilter && (
+
+ )}
+
+ {controls}
+
+
+ {rows.length === total
+ ? `${count(total)} ${total === 1 ? 'row' : 'rows'}`
+ : `${count(rows.length)} of ${count(total)} rows`}
+
+ {actions}
+
+
+ |
|---|
+ The view saved in this link may be out of date. The rest of the page is unaffected. +
+Reloading without the view saved in the link usually fixes it.
+{glossary.note}
} -{glossary.note}
} + > + )} +