-
-
-
- {bill.shortTitle ?? bill.title}
-
-
-
- {bill.final_judgment &&
}
+
+
+
+
+
+ Bill {bill.billID}
+ {bill.sponsorName && bill.sponsorName !== "Unknown" && (
+ · {bill.sponsorName}
+ )}
+
+
+ {bill.shortTitle ?? bill.title}
+
- {/* Description */}
- {bill.description && (
-
- {bill.description}
-
+ {bill.final_judgment && (
+
)}
+
- {/* Tags Section */}
-
- {/* Impact Badge */}
- {bill.impact && (
-
- {bill.impact} Impact
-
- )}
- {(bill.billID === "C-1" || bill.billID === "S-1") && (
-
- Pro Forma Bill
-
- )}
+ {bill.description && (
+
+ {bill.description}
+
+ )}
- {/* Genre Tags (limit to 3 visible) */}
- {bill.genres &&
- bill.genres.length > 0 &&
- bill.genres.map((genre, index) => {
- const icon = getCategoryIcon(genre);
- return (
- icon && (
-
- ["name"]}
- />{" "}
- {genre}
-
- )
- );
- })}
-
+
+ {bill.impact && {bill.impact} impact }
+ {isProForma && Pro forma }
+ {bill.genres?.map((genre, index) => {
+ const icon = getCategoryIcon(genre);
+ return (
+ icon && (
+
+ ["name"]}
+ />
+ {genre}
+
+ )
+ );
+ })}
+
- {/* Footer Section */}
-
-
- Bill {bill.billID} {" "}
- {bill.sponsorName && by {bill.sponsorName} }
-
-
{dateDisplay}
-
+
+
+ Read the analysis
+
+ {dateDisplay}
diff --git a/src/app/bills/components/FilterSection/filter-section.component.tsx b/src/app/bills/components/FilterSection/filter-section.component.tsx
index d2492600..fdd60d82 100644
--- a/src/app/bills/components/FilterSection/filter-section.component.tsx
+++ b/src/app/bills/components/FilterSection/filter-section.component.tsx
@@ -1,19 +1,8 @@
+"use client";
+
import { useState } from "react";
-import { Search, X, Filter } from "lucide-react";
-import { Card, CardContent, CardHeader, CardTitle } from "../ui/card";
-import { Badge } from "../ui/badge";
-import { Button } from "../ui/button";
-import { Label } from "../ui/label";
-import { Input } from "../ui/input";
-import { Checkbox } from "../ui/checkbox";
-import {
- Select,
- SelectContent,
- SelectItem,
- SelectTrigger,
- SelectValue,
-} from "../ui/select";
+import { cn } from "@/app/bills/lib/utils";
export interface FilterState {
search: string;
@@ -41,6 +30,96 @@ interface FilterSidebarProps {
filterOptions?: FilterOptions;
}
+type ArrayFilterKey = "status" | "category" | "party" | "chamber" | "judgement";
+
+/* A square, hairline checkbox in the house style — no rounded shadcn control.
+ The native input is kept for keyboard and screen-reader behaviour and drawn
+ with `appearance-none`; the tick is a `peer-checked` sibling. */
+function FilterCheckbox({
+ id,
+ label,
+ checked,
+ onChange,
+ className,
+}: {
+ id: string;
+ label: string;
+ checked: boolean;
+ onChange: () => void;
+ className?: string;
+}) {
+ return (
+
+
+
+
+
+
+
+
+ {label}
+
+
+ );
+}
+
+function FilterGroup({
+ label,
+ count,
+ children,
+}: {
+ label: string;
+ count: number;
+ children: React.ReactNode;
+}) {
+ return (
+
+
+
+ {label}
+ {count > 0 && {count} }
+
+
+
+
+
+ {children}
+
+ );
+}
+
export function FilterSidebar({
filters,
onFiltersChange,
@@ -68,10 +147,7 @@ export function FilterSidebar({
onFiltersChange({ ...filters, [key]: value });
};
- const toggleArrayFilter = (
- key: "status" | "category" | "party" | "chamber" | "judgement",
- value: string,
- ) => {
+ const toggleArrayFilter = (key: ArrayFilterKey, value: string) => {
const currentValues = filters[key];
const newValues = currentValues.includes(value)
? currentValues.filter((v) => v !== value)
@@ -79,209 +155,184 @@ export function FilterSidebar({
updateFilter(key, newValues);
};
- const getActiveFilterCount = () => {
- let count = 0;
- if (filters.search) count++;
- count += filters.status.length;
- count += filters.category.length;
- count += filters.party.length;
- count += filters.chamber.length;
- count += filters.judgement.length;
- if (filters.dateRange && filters.dateRange !== "all") count++;
- return count;
- };
-
- const activeCount = getActiveFilterCount();
+ const activeCount =
+ (filters.search ? 1 : 0) +
+ filters.status.length +
+ filters.category.length +
+ filters.party.length +
+ filters.chamber.length +
+ filters.judgement.length +
+ (filters.dateRange && filters.dateRange !== "all" ? 1 : 0);
return (
-
-
-
-
-
- Filters
- {activeCount > 0 && (
-
- {activeCount}
-
- )}
-
-
- {activeCount > 0 && (
-
- Clear All
-
- )}
-
+
+
+ Filters
+ {activeCount > 0 && (
+ {activeCount}
+ )}
+
+
+ {activeCount > 0 && (
+
- {isCollapsed ? "+" : "−"}
-
-
+ Clear all
+
+ )}
+
+ {isCollapsed ? "Show" : "Hide"}
+
-
+
{!isCollapsed && (
-
+
{/* Search */}
-
-
Search
-
-
-
+
+
+
+
+
+
updateFilter("search", e.target.value)}
- className="pl-9"
+ className="min-w-0 flex-1 bg-transparent font-mono text-sm outline-none placeholder:text-text-secondary"
/>
{filters.search && (
-
updateFilter("search", "")}
+ aria-label="Clear search"
+ className="shrink-0 cursor-pointer text-text-secondary hover:text-dark"
>
-
-
+
+
+
+
)}
- {/* Status */}
-
-
Status
-
- {(filterOptions?.statuses || []).map((status) => (
-
-
- toggleArrayFilter("status", status.toLowerCase())
- }
- />
-
- {status}
-
-
- ))}
-
-
+
+ {(["yes", "no", "abstain"] as const).map((j) => (
+ toggleArrayFilter("judgement", j)}
+ />
+ ))}
+
- {/* Judgement */}
-
-
Judgement
-
- {["yes", "no", "abstain"].map((j) => (
-
- toggleArrayFilter("judgement", j)}
- />
-
- {j}
-
-
- ))}
-
-
+
+ {(filterOptions?.statuses || []).map((status) => (
+
+ toggleArrayFilter("status", status.toLowerCase())
+ }
+ />
+ ))}
+
- {/* Category */}
-
-
Category
-
- {(filterOptions?.categories || []).map((category) => (
-
-
- toggleArrayFilter("category", category)
- }
- />
-
- {category}
-
-
- ))}
-
-
+
+ {(filterOptions?.categories || []).map((category) => (
+ toggleArrayFilter("category", category)}
+ />
+ ))}
+
- {/* Sponsor Party */}
-
-
Sponsor Party
-
- {(filterOptions?.parties || []).map((party) => (
-
- toggleArrayFilter("party", party)}
- />
-
- {party}
-
-
- ))}
-
-
+
+ {(filterOptions?.parties || []).map((party) => (
+ toggleArrayFilter("party", party)}
+ />
+ ))}
+
- {/* Chamber */}
-
-
Chamber
-
- {(filterOptions?.chambers || []).map((chamber) => (
-
-
- toggleArrayFilter("chamber", chamber)
- }
- />
-
- {chamber}
-
-
- ))}
-
-
+
+ {(filterOptions?.chambers || []).map((chamber) => (
+ toggleArrayFilter("chamber", chamber)}
+ />
+ ))}
+
- {/* Date Range */}
-
- Date Range
-
+
+ Introduced
+
+ updateFilter("dateRange", value)}
+ onChange={(e) => updateFilter("dateRange", e.target.value)}
+ className="mt-3 h-11 w-full cursor-pointer appearance-none border border-border-light bg-linen-50 px-3 type-mono-sm text-dark outline-none focus-visible:border-dark"
>
-
-
-
-
- All time
- Last month
- Last 3 months
- Last 6 months
- Last year
-
-
+ All time
+ Last month
+ Last 3 months
+ Last 6 months
+ Last year
+
-
+
)}
-
+
);
}
diff --git a/src/app/bills/components/Judgement/judgement.component.tsx b/src/app/bills/components/Judgement/judgement.component.tsx
index 686dd5b1..9c1ec308 100644
--- a/src/app/bills/components/Judgement/judgement.component.tsx
+++ b/src/app/bills/components/Judgement/judgement.component.tsx
@@ -38,8 +38,8 @@ const sizes: Record<
Size,
{ pad: string; text: string; icon: string; gap: string }
> = {
- sm: { pad: "p-1", text: "text-xs", icon: "h-4 w-4", gap: "gap-2" },
- md: { pad: "p-3", text: "text-base", icon: "h-5 w-5", gap: "gap-3" },
+ sm: { pad: "py-1", text: "type-label-sm", icon: "h-3.5 w-3.5", gap: "gap-1.5" },
+ md: { pad: "py-2.5", text: "type-label", icon: "h-4 w-4", gap: "gap-2" },
};
interface JudgementProps {
@@ -89,7 +89,7 @@ export function Judgement({
.join(" ")}
>
-
+
{verdictCopy(judgement)}
diff --git a/src/app/bills/components/ui/badge.tsx b/src/app/bills/components/ui/badge.tsx
deleted file mode 100644
index 2ccc2c44..00000000
--- a/src/app/bills/components/ui/badge.tsx
+++ /dev/null
@@ -1,46 +0,0 @@
-import * as React from "react";
-import { Slot } from "@radix-ui/react-slot";
-import { cva, type VariantProps } from "class-variance-authority";
-
-import { cn } from "./utils";
-
-const badgeVariants = cva(
- "inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
- {
- variants: {
- variant: {
- default:
- "border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
- secondary:
- "border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
- destructive:
- "border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
- outline:
- "text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
- },
- },
- defaultVariants: {
- variant: "default",
- },
- },
-);
-
-function Badge({
- className,
- variant,
- asChild = false,
- ...props
-}: React.ComponentProps<"span"> &
- VariantProps & { asChild?: boolean }) {
- const Comp = asChild ? Slot : "span";
-
- return (
-
- );
-}
-
-export { Badge, badgeVariants };
diff --git a/src/app/bills/components/ui/checkbox.tsx b/src/app/bills/components/ui/checkbox.tsx
deleted file mode 100644
index c81696bf..00000000
--- a/src/app/bills/components/ui/checkbox.tsx
+++ /dev/null
@@ -1,32 +0,0 @@
-"use client";
-
-import * as React from "react";
-import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
-import { CheckIcon } from "lucide-react";
-
-import { cn } from "./utils";
-
-function Checkbox({
- className,
- ...props
-}: React.ComponentProps) {
- return (
-
-
-
-
-
- );
-}
-
-export { Checkbox };
diff --git a/src/app/bills/components/ui/input.tsx b/src/app/bills/components/ui/input.tsx
deleted file mode 100644
index 19b0c619..00000000
--- a/src/app/bills/components/ui/input.tsx
+++ /dev/null
@@ -1,21 +0,0 @@
-import * as React from "react";
-
-import { cn } from "./utils";
-
-function Input({ className, type, ...props }: React.ComponentProps<"input">) {
- return (
-
- );
-}
-
-export { Input };
diff --git a/src/app/bills/components/ui/label.tsx b/src/app/bills/components/ui/label.tsx
deleted file mode 100644
index af250e57..00000000
--- a/src/app/bills/components/ui/label.tsx
+++ /dev/null
@@ -1,24 +0,0 @@
-"use client";
-
-import * as React from "react";
-import * as LabelPrimitive from "@radix-ui/react-label";
-
-import { cn } from "./utils";
-
-function Label({
- className,
- ...props
-}: React.ComponentProps) {
- return (
-
- );
-}
-
-export { Label };
diff --git a/src/app/bills/components/ui/select.tsx b/src/app/bills/components/ui/select.tsx
deleted file mode 100644
index 899692c6..00000000
--- a/src/app/bills/components/ui/select.tsx
+++ /dev/null
@@ -1,185 +0,0 @@
-"use client";
-
-import * as React from "react";
-import * as SelectPrimitive from "@radix-ui/react-select";
-import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react";
-
-import { cn } from "./utils";
-
-function Select({
- ...props
-}: React.ComponentProps) {
- return ;
-}
-
-function SelectGroup({
- ...props
-}: React.ComponentProps) {
- return ;
-}
-
-function SelectValue({
- ...props
-}: React.ComponentProps) {
- return ;
-}
-
-function SelectTrigger({
- className,
- size = "default",
- children,
- ...props
-}: React.ComponentProps & {
- size?: "sm" | "default";
-}) {
- return (
-
- {children}
-
-
-
-
- );
-}
-
-function SelectContent({
- className,
- children,
- position = "popper",
- ...props
-}: React.ComponentProps) {
- return (
-
-
-
-
- {children}
-
-
-
-
- );
-}
-
-function SelectLabel({
- className,
- ...props
-}: React.ComponentProps) {
- return (
-
- );
-}
-
-function SelectItem({
- className,
- children,
- ...props
-}: React.ComponentProps) {
- return (
-
-
-
-
-
-
- {children}
-
- );
-}
-
-function SelectSeparator({
- className,
- ...props
-}: React.ComponentProps) {
- return (
-
- );
-}
-
-function SelectScrollUpButton({
- className,
- ...props
-}: React.ComponentProps) {
- return (
-
-
-
- );
-}
-
-function SelectScrollDownButton({
- className,
- ...props
-}: React.ComponentProps) {
- return (
-
-
-
- );
-}
-
-export {
- Select,
- SelectContent,
- SelectGroup,
- SelectItem,
- SelectLabel,
- SelectScrollDownButton,
- SelectScrollUpButton,
- SelectSeparator,
- SelectTrigger,
- SelectValue,
-};
diff --git a/src/app/bills/page.tsx b/src/app/bills/page.tsx
index cf088c32..1b7b064e 100644
--- a/src/app/bills/page.tsx
+++ b/src/app/bills/page.tsx
@@ -2,14 +2,13 @@ import { BillSummary } from "./types";
import BillExplorer from "./BillExplorer";
import { getAllBillsFromDB } from "@/app/bills/server/get-all-bills-from-db";
import { fromBuildCanadaDbBill } from "@/app/bills/utils/billConverters";
-import { getParliament45Header } from "@/app/bills/components/BillDetail/BillHeader";
-import Markdown from "react-markdown";
import type { Metadata } from "next";
import { headers } from "next/headers";
import { env } from "@/app/bills/env";
import { buildRelativePath } from "@/app/bills/utils/basePath";
import { BUILD_CANADA_TWITTER_HANDLE, PROJECT_NAME } from "@/app/bills/consts/general";
import FAQModalTrigger from "./FAQModalTrigger";
+import { PageHeader } from "@/components/ui/page-header";
const CANADIAN_PARLIAMENT_NUMBER = 45;
type HomeSearchParams = { cache?: string };
@@ -223,23 +222,13 @@ export default async function Home({
}
const bills = await getMergedBillsCached();
return (
-
-
-
-
-
- 45th Canadian Federal Parliament
-
-
-
-
- {getParliament45Header()}
-
-
-
-
+
+
Bills — The 45th Parliament>}
+ description="Every federal bill before the House and the Senate, summarized and weighed against a pro-growth standard."
+ action={ }
+ />
+
);
}
diff --git a/src/components/ui/page-header.tsx b/src/components/ui/page-header.tsx
index 51a54235..a0ce818b 100644
--- a/src/components/ui/page-header.tsx
+++ b/src/components/ui/page-header.tsx
@@ -1,16 +1,21 @@
type PageHeaderProps = {
title: React.ReactNode;
description?: React.ReactNode;
+ /** Optional trailing control (a link, a dialog trigger) shown beside the title. */
+ action?: React.ReactNode;
};
-export function PageHeader({ title, description }: PageHeaderProps) {
+export function PageHeader({ title, description, action }: PageHeaderProps) {
return (
-
-
{title}
- {description && (
-
{description}
- )}
+
+
+
{title}
+ {description && (
+
{description}
+ )}
+
+ {action &&
{action}
}
);