Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
81 commits
Select commit Hold shift + click to select a range
b7a65ea
feat: Report builder
anth-volk Dec 19, 2025
0f50bfd
feat: Report meta panel
anth-volk Dec 22, 2025
ad5feaa
fix: Visual improvements to policy setup modal
anth-volk Dec 22, 2025
8975619
fix: Minor visual fixes
anth-volk Dec 22, 2025
18fb63f
feat: First implementation of existing policy selector modal
anth-volk Dec 22, 2025
ca309f2
feat: View user's existing policy details
anth-volk Dec 23, 2025
913a722
feat: First version of policy creation modal
anth-volk Dec 23, 2025
b6e1496
fix: Info panel fixes
anth-volk Dec 24, 2025
4ad61a5
fix: Various fixes to param creator
anth-volk Dec 26, 2025
a0abb6f
feat: Parameter search
anth-volk Dec 26, 2025
6dc9157
fix: Filter out pycache params
anth-volk Dec 26, 2025
1faf648
fix: Use proper policy loading structure
anth-volk Dec 26, 2025
ca0d337
fix: Properly use association data for household display
anth-volk Dec 26, 2025
829b125
fix: Fallback to Policy #X for policies with no label
anth-volk Dec 29, 2025
6d8e253
fix: Remove horizontal view
anth-volk Dec 29, 2025
2929ae5
feat: Usage tracking
anth-volk Dec 29, 2025
b4ff8ae
feat: Geography utils
anth-volk Feb 12, 2026
aed27e4
feat: PopulationBrowseModal
anth-volk Dec 29, 2025
b0449fb
feat: Population creation & selection modal
anth-volk Dec 29, 2025
0b8c108
fix: Remove dupe button
anth-volk Dec 29, 2025
1459355
feat: Move household creation into modal
anth-volk Dec 30, 2025
acc3afc
fix: Fixes to household creation modal
anth-volk Dec 30, 2025
56657d2
fix: Remove cancel button from household creation modal
anth-volk Dec 30, 2025
83718f2
fix: Properly create new policy
anth-volk Dec 31, 2025
4642d74
fix: Don't allow single-sim reports for subnational geographies
anth-volk Dec 31, 2025
0f287f7
chore: Refactor, part 1
anth-volk Dec 31, 2025
443fbb8
chore: Further refactor
anth-volk Dec 31, 2025
2b2872e
fix: Report builder modal improvements
anth-volk Jan 5, 2026
b794d66
fix: Improve US Congressional district formatting
anth-volk Jan 5, 2026
fb353ab
feat: Enable Run button
anth-volk Jan 5, 2026
859deba
feat: Add edit icon for names
anth-volk Jan 6, 2026
9c878f9
fix: Fix param value display
anth-volk Jan 6, 2026
03e3510
feat: Modify policy creation modal
anth-volk Jan 9, 2026
90c2a8c
fix: Remove param demos
anth-volk Feb 13, 2026
63e77ce
fix: Allow nameless policies
anth-volk Feb 13, 2026
229e188
fix: Fix dates, US nationwide populations
anth-volk Feb 13, 2026
a5a3ff3
feat: Builder variants
anth-volk Feb 13, 2026
e970bce
feat: Add places
anth-volk Feb 13, 2026
f5640d0
fix: Move to more streamlined design
anth-volk Feb 17, 2026
1ab08a0
fix: Modify top bar
anth-volk Feb 17, 2026
b089b7f
fix: Fix simulation label editing
anth-volk Feb 17, 2026
0a58cbe
feat: Modified report builder interface
anth-volk Feb 18, 2026
b2d0110
feat: Expand use cases that report builder supports
anth-volk Feb 18, 2026
f632f56
feat: Report modification screen
anth-volk Feb 19, 2026
40c9c70
fix: Harmonize competing hydrated report models
anth-volk Feb 19, 2026
b852e61
feat: Report modify submission, naming modal, Reports page columns, a…
anth-volk Feb 19, 2026
4616eb6
feat: First implementation of a policy modification structure
anth-volk Feb 20, 2026
3357734
feat: Policy editor footer layout, report view/edit modes, and UX ref…
anth-volk Feb 20, 2026
46c4716
fix: Year selector styling, button ordering, update icon, and overflo…
anth-volk Feb 20, 2026
a30f738
fix: Prevent EditableLabel from overflowing simulation card
anth-volk Feb 20, 2026
87a682f
feat: Clean up policy editing buttons in report builder
anth-volk Feb 20, 2026
76ce650
feat: Icon action buttons for tables, remove checkboxes, policy edito…
anth-volk Feb 20, 2026
1ff12e6
feat: Standardized action buttons, view/edit split, and consistent ic…
anth-volk Feb 23, 2026
1523bfe
feat: Simplify table actions and add view policy button
anth-volk Feb 23, 2026
afffbff
fix: Ensure policy modal respects initialEditorMode on open
anth-volk Feb 23, 2026
4e5762a
feat: Replace report creation pathway with report builder, polish pol…
anth-volk Feb 24, 2026
a16eb27
feat: Implement "Update existing policy" in policy modals
anth-volk Feb 24, 2026
6974835
fix: Use design tokens, fix title case, extract shared PolicyOverview…
anth-volk Feb 24, 2026
22695f1
fix: Consistent ingredient card padding, remove disabled account sett…
anth-volk Feb 25, 2026
6398705
fix: Reuse PolicyOverviewContent in policy details drawer
anth-volk Feb 25, 2026
fb08b7e
feat: Swap ingredient colors, default baseline, remove placeholders
anth-volk Feb 25, 2026
d8b2257
feat: Enter key submit, unified gear button, back nav, remove loading…
anth-volk Feb 26, 2026
efc3914
feat: Add Migration tab with collapsible sections, progressive distri…
anth-volk Feb 26, 2026
52783fd
feat: Migrate remaining UK charts, remove mockup page
anth-volk Feb 26, 2026
091f14a
feat: Remove tab ribbon, move compute button left of mode selector
anth-volk Feb 26, 2026
a7a45c7
feat: Restore back breadcrumb, add view/edit policy gear button
anth-volk Feb 26, 2026
7dc78a3
feat: Remove /migration URL, add Overview section, collapse all but O…
anth-volk Feb 26, 2026
0632ff7
fix: Move reproduce button to right of view/edit button
anth-volk Feb 26, 2026
01f164d
feat: Add expandable DashboardCard component, playground page, and re…
anth-volk Feb 27, 2026
e11ac5b
feat: Fix DashboardCard sizing, add chart height props, and budgetary…
anth-volk Mar 2, 2026
4ecf9fc
feat: Add congressional district card, expandable controls row, and f…
anth-volk Mar 3, 2026
1827a1c
feat: Remove congressional dropdown, fill map to expanded card height
anth-volk Mar 3, 2026
50ea627
feat: Add error state visualization, improve error logging, and UI fixes
anth-volk Mar 3, 2026
a1f1579
feat: Redesign dashboard cards with district rankings, waterfall char…
anth-volk Mar 6, 2026
97a79d1
fix: Update stories and fix type errors after rebase
anth-volk Mar 8, 2026
d6eab19
chore: Remove playground and policy editing concepts pages
anth-volk Mar 8, 2026
f04ae73
refactor: Migrate report output components from Mantine to shadcn/ui
anth-volk Mar 8, 2026
8a4158d
refactor: Migrate report builder from Mantine to shadcn/ui, add stori…
anth-volk Mar 9, 2026
5d57cc9
fix: Resolve csstype type conflicts and ignore storybook-static in st…
anth-volk Mar 9, 2026
eb10ffc
fix: Resolve 36 test failures after Mantine-to-shadcn migration
anth-volk Mar 9, 2026
acb7d85
fix: Revert lazy loading in router, darken breadcrumb color
anth-volk Mar 9, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion .github/workflows/chromatic.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,12 @@ jobs:

- name: Run Chromatic
working-directory: ./app
run: bunx chromatic --only-changed ${{ github.event_name == 'push' && '--auto-accept-changes' || '' }} --build-script-name="storybook:build" --project-token=${{ secrets.CHROMATIC_PROJECT_TOKEN }}
run: >-
bunx chromatic
--only-changed
--build-script-name="storybook:build"
--project-token=${{ secrets.CHROMATIC_PROJECT_TOKEN }}
${{ github.event_name == 'push' && '--auto-accept-changes' || '' }}
${{ github.event_name == 'pull_request' && format('--patch-build={0}...main', github.head_ref) || '' }}
env:
VITE_APP_MODE: website
1 change: 1 addition & 0 deletions app/.stylelintignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
dist
storybook-static
48 changes: 22 additions & 26 deletions app/src/CalculatorRouter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,32 +2,24 @@
* Router for the Calculator app (app.policyengine.org)
* Contains only the interactive calculator functionality
*/
import { lazy } from 'react';
import { createBrowserRouter, Navigate, RouterProvider } from 'react-router-dom';
import { createBrowserRouter, Navigate, Outlet, RouterProvider } from 'react-router-dom';
import PathwayLayout from './components/PathwayLayout';
import StandardLayout from './components/StandardLayout';
import NotFoundPage from './pages/NotFound.page';
import PoliciesPage from './pages/Policies.page';
import PopulationsPage from './pages/Populations.page';
import ReportOutputPage from './pages/ReportOutput.page';
import ReportsPage from './pages/Reports.page';
import SimulationsPage from './pages/Simulations.page';
import PolicyPathwayWrapper from './pathways/policy/PolicyPathwayWrapper';
import PopulationPathwayWrapper from './pathways/population/PopulationPathwayWrapper';
import SimulationPathwayWrapper from './pathways/simulation/SimulationPathwayWrapper';
import ReportBuilderPage from './pages/reportBuilder/ReportBuilderPage';
import ModifyReportPage from './pages/reportBuilder/ModifyReportPage';
import { CountryGuard } from './routing/guards/CountryGuard';
import { MetadataGuard } from './routing/guards/MetadataGuard';
import { MetadataLazyLoader } from './routing/guards/MetadataLazyLoader';
import { RedirectToCountry } from './routing/RedirectToCountry';
import SuspenseOutlet from './routing/SuspenseOutlet';

// Lazy-loaded page components — only fetched when the route is visited
const PoliciesPage = lazy(() => import('./pages/Policies.page'));
const PopulationsPage = lazy(() => import('./pages/Populations.page'));
const ReportOutputPage = lazy(() => import('./pages/ReportOutput.page'));
const ReportsPage = lazy(() => import('./pages/Reports.page'));
const SimulationsPage = lazy(() => import('./pages/Simulations.page'));

// Lazy-loaded pathway wrappers — heavy components with their own sub-routes
const PolicyPathwayWrapper = lazy(() => import('./pathways/policy/PolicyPathwayWrapper'));
const PopulationPathwayWrapper = lazy(
() => import('./pathways/population/PopulationPathwayWrapper')
);
const ReportPathwayWrapper = lazy(() => import('./pathways/report/ReportPathwayWrapper'));
const SimulationPathwayWrapper = lazy(
() => import('./pathways/simulation/SimulationPathwayWrapper')
);

/**
* Layout wrapper that renders StandardLayout with Outlet for nested routes.
Expand All @@ -37,7 +29,7 @@ const SimulationPathwayWrapper = lazy(
function StandardLayoutOutlet() {
return (
<StandardLayout>
<SuspenseOutlet />
<Outlet />
</StandardLayout>
);
}
Expand Down Expand Up @@ -68,12 +60,8 @@ const router = createBrowserRouter(
},
// Pathway routes - pathways manage their own layouts
{
element: <SuspenseOutlet />,
element: <PathwayLayout />,
children: [
{
path: 'reports/create',
element: <ReportPathwayWrapper />,
},
{
path: 'simulations/create',
element: <SimulationPathwayWrapper />,
Expand Down Expand Up @@ -117,6 +105,14 @@ const router = createBrowserRouter(
path: 'policies',
element: <PoliciesPage />,
},
{
path: 'reports/create',
element: <ReportBuilderPage />,
},
{
path: 'reports/create/:userReportId',
element: <ModifyReportPage />,
},
{
path: 'account',
element: <div>Account settings page</div>,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@ export function buildDistrictLabelLookup(regions: MetadataRegionEntry[]): Distri

for (const region of regions) {
if (region.type === US_REGION_TYPES.CONGRESSIONAL_DISTRICT) {
lookup.set(region.name, region.label);
// Strip "congressional_district/" prefix so keys match API district IDs (e.g., "AL-01")
const key = region.name.replace(/^congressional_district\//, '');
lookup.set(key, region.label);
}
}

Expand Down
16 changes: 12 additions & 4 deletions app/src/api/societyWideCalculation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,20 @@ export async function fetchSocietyWideCalculation(
});

if (!response.ok) {
let body = '';
try {
body = await response.text();
} catch {
// ignore
}
console.error(
'[fetchSocietyWideCalculation] Failed with status:',
response.status,
response.statusText
`[fetchSocietyWideCalculation] ${response.status} ${response.statusText}`,
url,
body
);
throw new Error(
`Society-wide calculation failed (${response.status}): ${body || response.statusText}`
);
throw new Error(`Society-wide calculation failed: ${response.statusText}`);
}

const data = await response.json();
Expand Down
103 changes: 103 additions & 0 deletions app/src/api/usageTracking.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
/**
* Usage Tracking Store
*
* A lightweight system for tracking "last used" timestamps for any ingredient
* type (policies, households, geographies, etc.).
*
* This is separate from association data - it only tracks when items
* were last accessed, not the items themselves.
*
* Usage:
* import { policyUsageStore } from '@/api/usageTracking';
*
* // Record that a policy was used
* policyUsageStore.recordUsage(policyId);
*
* // Get 5 most recently used policy IDs
* const recentIds = policyUsageStore.getRecentIds(5);
*/

/** ISO timestamp string */
export type UsageData = Record<string, string>;

/**
* Generic store for tracking usage of items by ID.
* Each ingredient type gets its own store instance with a unique storage key.
*/
export class UsageTrackingStore {
constructor(private readonly storageKey: string) {}

/**
* Record that an item was used/accessed.
* Updates the lastUsedAt timestamp.
*/
recordUsage(id: string): string {
const usage = this.getAll();
const timestamp = new Date().toISOString();
usage[id] = timestamp;
localStorage.setItem(this.storageKey, JSON.stringify(usage));
return timestamp;
}

/**
* Get all usage records (id -> lastUsedAt timestamp).
*/
getAll(): UsageData {
try {
const stored = localStorage.getItem(this.storageKey);
return stored ? JSON.parse(stored) : {};
} catch {
console.error(`[UsageTrackingStore] Failed to parse ${this.storageKey}`);
return {};
}
}

/**
* Get IDs sorted by most recently used.
* @param limit Maximum number of IDs to return (default 10)
*/
getRecentIds(limit = 10): string[] {
const usage = this.getAll();
return Object.entries(usage)
.sort(([, a], [, b]) => b.localeCompare(a))
.slice(0, limit)
.map(([id]) => id);
}

/**
* Get the last used timestamp for a specific ID.
*/
getLastUsed(id: string): string | null {
return this.getAll()[id] || null;
}

/**
* Check if an item has any usage recorded.
*/
hasUsage(id: string): boolean {
return !!this.getAll()[id];
}

/**
* Remove usage record for a specific ID.
*/
removeUsage(id: string): void {
const usage = this.getAll();
delete usage[id];
localStorage.setItem(this.storageKey, JSON.stringify(usage));
}

/**
* Clear all usage records for this store.
*/
clear(): void {
localStorage.removeItem(this.storageKey);
}
}

// Pre-configured stores for each ingredient type
export const policyUsageStore = new UsageTrackingStore('policy-usage');
export const householdUsageStore = new UsageTrackingStore('household-usage');
export const geographyUsageStore = new UsageTrackingStore('geography-usage');
export const simulationUsageStore = new UsageTrackingStore('simulation-usage');
export const reportUsageStore = new UsageTrackingStore('report-usage');
3 changes: 0 additions & 3 deletions app/src/components/IngredientReadView.story.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,6 @@ const meta: Meta<typeof IngredientReadView> = {
component: IngredientReadView,
args: {
onBuild: () => {},
enableSelection: true,
isSelected: () => false,
onSelectionChange: () => {},
},
};

Expand Down
71 changes: 12 additions & 59 deletions app/src/components/IngredientReadView.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { IconPlus } from '@tabler/icons-react';
import {
Button,
Checkbox,
Spinner,
ShadcnTable as Table,
TableBody,
Expand Down Expand Up @@ -31,9 +30,6 @@ interface IngredientReadViewProps {
searchValue?: string;
onSearchChange?: (value: string) => void;
onMoreFilters?: () => void;
enableSelection?: boolean;
isSelected?: (recordId: string) => boolean;
onSelectionChange?: (recordId: string, selected: boolean) => void;
}

export default function IngredientReadView({
Expand All @@ -50,9 +46,6 @@ export default function IngredientReadView({
searchValue: _searchValue = '',
onSearchChange: _onSearchChange,
onMoreFilters: _onMoreFilters,
enableSelection = true,
isSelected = () => false,
onSelectionChange,
}: IngredientReadViewProps) {
return (
<div>
Expand Down Expand Up @@ -135,16 +128,6 @@ export default function IngredientReadView({
<Table>
<TableHeader>
<TableRow style={{ backgroundColor: colors.gray[50] }}>
{enableSelection && (
<TableHead
style={{
width: '48px',
padding: `${spacing.md} ${spacing.lg}`,
}}
>
{/* Optional: Add "select all" checkbox here in the future */}
</TableHead>
)}
{columns.map((column) => (
<TableHead
key={column.key}
Expand All @@ -163,48 +146,18 @@ export default function IngredientReadView({
</TableRow>
</TableHeader>
<TableBody>
{data.map((record) => {
const selected = isSelected(record.id);
return (
<TableRow
key={record.id}
style={{
backgroundColor: selected ? colors.blue[50] : 'transparent',
borderLeft: selected
? `3px solid ${colors.primary[500]}`
: '3px solid transparent',
cursor: enableSelection ? 'pointer' : 'default',
}}
onClick={() => {
if (enableSelection && onSelectionChange) {
onSelectionChange(record.id, !selected);
}
}}
>
{enableSelection && (
<TableCell style={{ padding: `${spacing.md} ${spacing.lg}` }}>
<Checkbox
checked={selected}
onCheckedChange={(checked) => {
if (onSelectionChange) {
onSelectionChange(record.id, !!checked);
}
}}
onClick={(e) => e.stopPropagation()}
/>
</TableCell>
)}
{columns.map((column) => (
<TableCell
key={column.key}
style={{ padding: `${spacing.md} ${spacing.lg}` }}
>
<ColumnRenderer config={column} record={record} />
</TableCell>
))}
</TableRow>
);
})}
{data.map((record) => (
<TableRow key={record.id}>
{columns.map((column) => (
<TableCell
key={column.key}
style={{ padding: `${spacing.md} ${spacing.lg}` }}
>
<ColumnRenderer config={column} record={record} />
</TableCell>
))}
</TableRow>
))}
</TableBody>
</Table>
)}
Expand Down
30 changes: 30 additions & 0 deletions app/src/components/columns/ActionsColumn.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { Button } from '@/components/ui/button';
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
import { ActionsColumnConfig, IngredientRecord } from './types';

interface ActionsColumnProps {
config: ActionsColumnConfig;
record: IngredientRecord;
}

export function ActionsColumn({ config, record }: ActionsColumnProps) {
return (
<div className="tw:flex tw:gap-1 tw:justify-end">
{config.actions.map((action) => (
<Tooltip key={action.action}>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
onClick={() => config.onAction(action.action, record.id)}
aria-label={action.tooltip}
>
{action.icon}
</Button>
</TooltipTrigger>
<TooltipContent side="bottom">{action.tooltip}</TooltipContent>
</Tooltip>
))}
</div>
);
}
Loading
Loading