Skip to content
2 changes: 1 addition & 1 deletion app/src/components/flagship/CalibrationMatches.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,7 @@ export function CalibrationMatchSection({
return card(<ValidationNote>Calibration checks are temporarily unavailable.</ValidationNote>);
}
if (matches === undefined) {
return card(<ValidationLoading>Checking calibration targets…</ValidationLoading>);
return card(<ValidationLoading label="Checking calibration targets" />);
}
if (matches.matches.length === 0) {
const where = matches.geography === 'US' ? 'nationally' : `in ${matches.geography}`;
Expand Down
16 changes: 15 additions & 1 deletion app/src/components/flagship/ReportAdjustPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,11 @@ interface ReportAdjustPanelProps {
year?: string;
/** What the report is compared with, so a recompute is too; current law when unset. */
baseline?: RunReportBaseline;
/**
* The report's baseline policy could not be loaded. A recompute would
* compare with current law instead, so it is off.
*/
baselineUnknown?: boolean;
}

/** True when the reform's parameter set is exactly the given provisions. */
Expand Down Expand Up @@ -66,6 +71,7 @@ export default function ReportAdjustPanel({
region,
year,
baseline,
baselineUnknown = false,
}: ReportAdjustPanelProps) {
const runReport = useRunFlagshipReport();
const nav = useAppNavigate();
Expand Down Expand Up @@ -351,7 +357,15 @@ export default function ReportAdjustPanel({
{reconcileError || runReport.error}
</Text>
)}
<Button onClick={recompute} disabled={!isDirty || busy || active.length === 0}>
{baselineUnknown && (
<Text style={{ fontSize: typography.fontSize.xs, color: colors.text.secondary }}>
The baseline policy could not be loaded, so a recompute cannot compare with it.
</Text>
)}
<Button
onClick={recompute}
disabled={baselineUnknown || !isDirty || busy || active.length === 0}
>
<IconChartBar size={16} />
{busy ? 'Recomputing…' : 'Recompute'}
</Button>
Expand Down
22 changes: 15 additions & 7 deletions app/src/components/flagship/ValidationDisplay.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { CSSProperties, ReactNode } from 'react';
import { IconExternalLink } from '@tabler/icons-react';
import { Badge, Spinner, Stack, Text } from '@/components/ui';
import { SkeletonLine, SkeletonRegion } from '@/components/flagship/states';
import { Badge, Stack, Text } from '@/components/ui';
import { colors, spacing, typography } from '@/designTokens';

/**
Expand Down Expand Up @@ -158,13 +159,20 @@ export function NotComparable({ children }: { children: ReactNode }) {
);
}

/** A card's check is still loading; the line, not the spinner, is what gets announced. */
export function ValidationLoading({ children }: { children: ReactNode }) {
/**
* A card's check is still loading: placeholder lines where its rows will
* be, as the rest of app v3 waits, named for what is coming.
*/
export function ValidationLoading({ label }: { label: string }) {
return (
<Stack role="status" style={{ flexDirection: 'row', alignItems: 'center', gap: spacing.sm }}>
<Spinner size="sm" role="presentation" aria-hidden="true" />
<ValidationNote>{children}</ValidationNote>
</Stack>
<SkeletonRegion
label={label}
style={{ display: 'flex', flexDirection: 'column', gap: spacing.sm }}
>
<SkeletonLine width="70%" />
<SkeletonLine width="90%" />
<SkeletonLine width="55%" />
</SkeletonRegion>
);
}

Expand Down
2 changes: 1 addition & 1 deletion app/src/components/flagship/ValidationPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -336,7 +336,7 @@ export function BaselineComparisons({
} else if (entries.length > 0) {
content = <ScorecardClaimTable label="Baseline comparisons" entries={entries} />;
} else if (trackRecord === undefined || scorecardRows === undefined) {
content = <ValidationLoading>Loading baseline comparisons…</ValidationLoading>;
content = <ValidationLoading label="Loading baseline comparisons" />;
} else if (trackRecord === null) {
content = <ValidationNote>Baseline comparisons are temporarily unavailable.</ValidationNote>;
} else if (trackRecord.reachedCount > 0) {
Expand Down
9 changes: 8 additions & 1 deletion app/src/components/flagship/reform/BaselinePicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -175,10 +175,17 @@ export default function BaselinePicker({ value, onChange, reforms }: BaselinePic
/>
</button>
</PopoverTrigger>
<PopoverContent align="start" style={{ width: 300, padding: 0 }}>
<PopoverContent
align="start"
// On a phone each choice is a fingertip tall.
className="tw:max-sm:[&_[data-slot=command-item]]:min-h-10"
style={{ width: 300, padding: 0 }}
>
<Command filter={nameFilter} label="Find a saved reform">
{reforms.length > 0 && (
<CommandInput
// 16px on a phone, so the browser does not zoom in to type.
className="tw:max-sm:text-base"
placeholder="Find a saved reform…"
value={search}
onValueChange={setSearch}
Expand Down
4 changes: 2 additions & 2 deletions app/src/components/flagship/reform/DraftTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,9 @@ export const ROW_GRID =
'tw:grid tw:items-center tw:gap-3 tw:@max-[480px]:gap-y-2 tw:grid-cols-[minmax(0,1fr)_auto] tw:@min-[480px]:grid-cols-[minmax(0,1fr)_150px_64px] tw:@min-[640px]:grid-cols-[minmax(0,1fr)_110px_150px_136px_64px]';
export const WIDE_ONLY = 'tw:hidden tw:@min-[640px]:block';
/** The value cell, under the name across the row on a phone's table. */
const VALUE_CELL = 'tw:min-w-0 tw:@max-[480px]:col-span-2 tw:@max-[480px]:row-start-2';
export const VALUE_CELL = 'tw:min-w-0 tw:@max-[480px]:col-span-2 tw:@max-[480px]:row-start-2';
/** The row's own buttons, beside the name on a phone's table, a fingertip each. */
const ACTIONS_CELL = 'tw:@max-[480px]:col-start-2 tw:@max-[480px]:row-start-1';
export const ACTIONS_CELL = 'tw:@max-[480px]:col-start-2 tw:@max-[480px]:row-start-1';
const ROW_BUTTON =
'tw:@max-[480px]:size-10 tw:@max-[480px]:items-center tw:@max-[480px]:justify-center';

Expand Down
12 changes: 8 additions & 4 deletions app/src/components/flagship/reform/ParametersStatus.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { ErrorState, SkeletonLine, SkeletonRegion } from '@/components/flagship/
import { colors, spacing, typography } from '@/designTokens';
import { useFlagshipMetadataStatus } from '@/hooks/useFlagshipMetadataStatus';
import { groupKeyFromPath } from '@/libs/flagship/parameterGroups';
import { ROW_GRID, WIDE_ONLY } from './DraftTable';
import { ACTIONS_CELL, ROW_GRID, VALUE_CELL, WIDE_ONLY } from './DraftTable';

/** The search bar and its filter row, as placeholders at their own size. */
export function SearchBarSkeleton() {
Expand Down Expand Up @@ -65,7 +65,9 @@ function DraftTableSkeleton({ rows }: { rows: number }) {
<span className={WIDE_ONLY} style={headerCell}>
Current law
</span>
<span style={headerCell}>New value</span>
<span className="tw:@max-[480px]:hidden" style={headerCell}>
New value
</span>
<span className={WIDE_ONLY} style={headerCell}>
Effective
</span>
Expand All @@ -84,11 +86,13 @@ function DraftTableSkeleton({ rows }: { rows: number }) {
<span className={WIDE_ONLY}>
<SkeletonLine width="70%" />
</span>
<SkeletonLine height={36} />
<div className={VALUE_CELL}>
<SkeletonLine height={36} />
</div>
<span className={WIDE_ONLY}>
<SkeletonLine width="80%" height={12} />
</span>
<span />
<span className={ACTIONS_CELL} />
</div>
))}
<div
Expand Down
85 changes: 22 additions & 63 deletions app/src/components/flagship/report/ReportDetailsError.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { IconAlertTriangle } from '@tabler/icons-react';
import { ErrorState } from '@/components/flagship/states';
import { Button } from '@/components/ui';
import { useAppNavigate } from '@/contexts/NavigationContext';
import { colors, spacing, typography } from '@/designTokens';
Expand All @@ -23,68 +23,27 @@ export default function ReportDetailsError({
const newer = error.code === 'newer-version';

return (
<section
role="alert"
className="tw:flex tw:flex-col"
style={{
gap: spacing.md,
maxWidth: 640,
padding: spacing.xl,
border: `1px solid ${colors.border.light}`,
borderRadius: spacing.radius.feature,
background: colors.background.primary,
fontFamily: typography.fontFamily.primary,
}}
<ErrorState
title="Could not read this report's saved details"
description={
newer
? 'A newer version of PolicyEngine saved them. Reload to update.'
: 'They are damaged in this browser.'
}
// Only a newer version's details read after a reload.
onRetry={newer ? () => window.location.reload() : undefined}
retryLabel="Reload"
actions={
<>
<Button variant={newer ? 'outline' : 'default'} onClick={onOpenWithout}>
Open without saved details
</Button>
<Button variant="outline" onClick={() => nav.push(`/${countryId}/build`)}>
Back to Build
</Button>
</>
}
>
<div className="tw:flex tw:items-start" style={{ gap: spacing.md }}>
<span
aria-hidden
className="tw:inline-flex tw:shrink-0 tw:rounded-full"
style={{
padding: spacing.sm,
// The warning yellow, tinted.
background: `${colors.warning}33`,
color: colors.text.warning,
}}
>
<IconAlertTriangle size={20} />
</span>
<div className="tw:flex tw:flex-col" style={{ gap: spacing.xs }}>
<h2
style={{
margin: 0,
fontSize: typography.fontSize.lg,
fontWeight: typography.fontWeight.semibold,
color: colors.text.primary,
}}
>
This report's saved details can't be read
</h2>
<p
style={{
margin: 0,
fontSize: typography.fontSize.sm,
lineHeight: typography.lineHeight.relaxed,
color: colors.text.secondary,
}}
>
{newer
? 'A newer version of PolicyEngine saved them. Reload to update.'
: 'They are damaged in this browser.'}
</p>
</div>
</div>

<div className="tw:flex tw:flex-wrap" style={{ gap: spacing.sm }}>
{newer && <Button onClick={() => window.location.reload()}>Reload</Button>}
<Button variant={newer ? 'outline' : 'default'} onClick={onOpenWithout}>
Open without saved details
</Button>
<Button variant="outline" onClick={() => nav.push(`/${countryId}/build`)}>
Back to Build
</Button>
</div>

<details>
<summary
className="tw:cursor-pointer"
Expand All @@ -108,6 +67,6 @@ export default function ReportDetailsError({
{error.message}
</pre>
</details>
</section>
</ErrorState>
);
}
2 changes: 1 addition & 1 deletion app/src/components/flagship/report/ReportValidation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ function ExternalEstimates({
if (!estimates && reforms.length === 0) {
fallback =
scorecard === undefined ? (
<ValidationLoading>Loading external estimates…</ValidationLoading>
<ValidationLoading label="Loading external estimates" />
) : scorecard === null ? (
<ValidationNote>External estimates are temporarily unavailable.</ValidationNote>
) : (
Expand Down
7 changes: 7 additions & 0 deletions app/src/components/flagship/report/ReportView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,11 @@ export interface ReportViewProps {
provisionsLoading?: boolean;
/** What the results are compared with, by name; current law when unset. */
customBaselineLabel?: string | null;
/**
* The baseline policy could not be loaded: the adjust panel cannot
* recompute against it.
*/
baselineUnknown?: boolean;
/** What the adjust panel's runs compare with; current law when unset. */
baseline?: RunReportBaseline;
year: string;
Expand Down Expand Up @@ -133,6 +138,7 @@ export default function ReportView({
priorLawBaseline = false,
provisionsLoading = false,
customBaselineLabel,
baselineUnknown = false,
baseline,
year,
region,
Expand Down Expand Up @@ -429,6 +435,7 @@ export default function ReportView({
region={region}
year={year}
baseline={baseline}
baselineUnknown={baselineUnknown}
/>
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { useState } from 'react';
import { IconAlertTriangle } from '@tabler/icons-react';
import HouseholdForm from '@/components/flagship/report/household/HouseholdForm';
import HouseholdImpactResult from '@/components/flagship/report/household/HouseholdImpactResult';
import RunProgress from '@/components/flagship/report/RunProgress';
import { Button, Stack, Text } from '@/components/ui';
import { ErrorState } from '@/components/flagship/states';
import { Stack, Text } from '@/components/ui';
import { colors, spacing, typography } from '@/designTokens';
import { useFlagshipHousehold } from '@/hooks/useFlagshipHousehold';
import {
Expand Down Expand Up @@ -81,46 +81,17 @@ export default function HouseholdImpactSection({
{running && <RunProgress title="Calculating this household" expectedMs={EXPECTED_MS} />}

{failed && (
<section
role="alert"
style={{
display: 'flex',
alignItems: 'flex-start',
gap: spacing.md,
padding: spacing.lg,
border: `1px solid ${colors.border.light}`,
borderRadius: spacing.radius.container,
background: colors.background.primary,
}}
>
<IconAlertTriangle size={20} aria-hidden style={{ flexShrink: 0, color: colors.error }} />
<Stack style={{ gap: spacing.sm, alignItems: 'flex-start', minWidth: 0 }}>
<Text
style={{
margin: 0,
fontSize: typography.fontSize.sm,
fontWeight: typography.fontWeight.medium,
}}
>
This household could not be calculated.
</Text>
{run.error?.message && (
<Text
style={{
margin: 0,
fontSize: typography.fontSize.xs,
color: colors.text.secondary,
overflowWrap: 'anywhere',
}}
>
{run.error.message}
</Text>
)}
<Button size="sm" variant="outline" onClick={() => void run.refetch()}>
Try again
</Button>
</Stack>
</section>
<ErrorState
compact
title="This household could not be calculated"
description={
// An API message can carry a long unbroken path.
run.error?.message && (
<span style={{ overflowWrap: 'anywhere' }}>{run.error.message}</span>
)
}
onRetry={run.refetch}
/>
)}

{result && requested && (
Expand Down
Loading
Loading