From 9e71670f39d85c6fc48f56e15586c6e328f84507 Mon Sep 17 00:00:00 2001 From: Mikaal Naik Date: Wed, 16 Sep 2026 13:16:33 -0400 Subject: [PATCH] Dress /bills in the house style MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The bills index was a shadcn island: rounded cards floating on the linen canvas, a boxed filter sidebar, and type that ignored the brand scale. It read as a different product from /memos or /tracker. Rebuild the page out of the same parts every other index uses — the bordered page frame, the shared PageHeader, hairline-ruled sections on a 1080px measure — and give the two data-heavy regions a real surface: white panels on the linen ground, the way /tracker already does it, so the page is not beige on beige. The filter sidebar drops shadcn Card/Input/Checkbox/Select for house markup: square appearance-none checkboxes, type-label group headings in
disclosures reusing the site's answer-reveal transition, and a native select. Its props are unchanged. The five primitives that left behind no other caller are deleted. PageHeader grows an optional `action` slot so the FAQ trigger can sit beside the title; existing callers are untouched. Co-Authored-By: Claude Opus 5 (1M context) --- src/app/bills/BillExplorer.tsx | 75 +-- src/app/bills/FAQModalTrigger.tsx | 44 +- src/app/bills/components/BillCard.tsx | 144 +++--- .../filter-section.component.tsx | 441 ++++++++++-------- .../Judgement/judgement.component.tsx | 6 +- src/app/bills/components/ui/badge.tsx | 46 -- src/app/bills/components/ui/checkbox.tsx | 32 -- src/app/bills/components/ui/input.tsx | 21 - src/app/bills/components/ui/label.tsx | 24 - src/app/bills/components/ui/select.tsx | 185 -------- src/app/bills/page.tsx | 27 +- src/components/ui/page-header.tsx | 17 +- 12 files changed, 403 insertions(+), 659 deletions(-) delete mode 100644 src/app/bills/components/ui/badge.tsx delete mode 100644 src/app/bills/components/ui/checkbox.tsx delete mode 100644 src/app/bills/components/ui/input.tsx delete mode 100644 src/app/bills/components/ui/label.tsx delete mode 100644 src/app/bills/components/ui/select.tsx diff --git a/src/app/bills/BillExplorer.tsx b/src/app/bills/BillExplorer.tsx index e6f6596b..ef306737 100644 --- a/src/app/bills/BillExplorer.tsx +++ b/src/app/bills/BillExplorer.tsx @@ -3,6 +3,7 @@ import { useEffect, useMemo, useState, useCallback, memo } from "react"; import { BillSummary } from "./types"; import BillCard from "@/app/bills/components/BillCard"; +import SectionLabel from "@/components/SectionLabel"; import { FilterSidebar, FilterState, @@ -264,39 +265,51 @@ function BillExplorer({ bills }: BillExplorerProps) { }, []); return ( -
-
- - -
- {filteredBills.length === 0 ? ( -
No bills match your filters.
- ) : ( -
    - {filteredBills.map((bill) => ( - +
    +
    + + +
    +
    + Bills + + {filteredBills.length}{" "} + {filteredBills.length === 1 ? "bill" : "bills"} + +
    + + {filteredBills.length === 0 ? ( +

    + No bills match your filters. +

    + ) : ( +
      + {filteredBills.map((bill) => ( + - ))} -
    - )} -
+ /> + ))} + + )} +
+
- + ); } diff --git a/src/app/bills/FAQModalTrigger.tsx b/src/app/bills/FAQModalTrigger.tsx index c6359994..49476428 100644 --- a/src/app/bills/FAQModalTrigger.tsx +++ b/src/app/bills/FAQModalTrigger.tsx @@ -53,7 +53,7 @@ export default function FAQModalTrigger() { -
-

Why did you build this?

-

+

+

Why did you build this?

+

We built this tool so that Canadians could easily understand parliamentary bills and how they align with a pro-growth stance.

-

+

Where does the bill data come from?

-

+

We are powered by{" "} The Civics Project @@ -117,32 +117,32 @@ export default function FAQModalTrigger() { parliamentary data feeds.

-

+

How do you determine a bill's judgement?

-

+

Each bill is evaluated against a set of pro-growth principles using an LLM. The LLM uses these principles to make a judgement on the bill. We show all of this, including the principles, evaluation and rationale, on the bill's page.

-

+

Was this tool created to vote on bills?

-

+

No. This tool was created to help Canadians better understand bills and their economic impact. It is meant to be educational and informative.

-

+

What prompt do you use to evaluate the bills?

-

+

This project, including the prompt, is open sourced on{" "} -

How can I contribute?

-

+

How can I contribute?

+

This is a work in progress and we would love help from others. Join us on{" "} -

How can I get in touch?

-

+

How can I get in touch?

+

You can reach out to us at{" "} - + hi@buildcanada.com

diff --git a/src/app/bills/components/BillCard.tsx b/src/app/bills/components/BillCard.tsx index 2b3a2e31..77c9d489 100644 --- a/src/app/bills/components/BillCard.tsx +++ b/src/app/bills/components/BillCard.tsx @@ -13,94 +13,88 @@ interface BillCardProps { bill: BillSummary & { tenet_evaluations?: TenetEvaluation[] }; } +/* A hairline tag in the house style: square, mono, uppercase. */ +function Tag({ + children, + emphasis = false, +}: { + children: React.ReactNode; + emphasis?: boolean; +}) { + return ( + + {children} + + ); +} + function BillCard({ bill }: BillCardProps) { const bestDate = getBillMostRecentDate(bill); const dateDisplay = formatBillDate(bestDate); const judgementValue: JudgementValue = bill.final_judgment || "abstain"; + const isProForma = bill.billID === "C-1" || bill.billID === "S-1"; return ( -
  • - -
    - {/* Header Section */} -
    -
    -
    -

    - {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 ( +
    + + + + + +
    + ); +} + +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 + + )} +
    -
    +
    {!isCollapsed && ( - +
    {/* 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 && ( - + + + + )}
    - {/* Status */} -
    - -
    - {(filterOptions?.statuses || []).map((status) => ( -
    - - toggleArrayFilter("status", status.toLowerCase()) - } - /> - -
    - ))} -
    -
    + + {(["yes", "no", "abstain"] as const).map((j) => ( + toggleArrayFilter("judgement", j)} + /> + ))} + - {/* Judgement */} -
    - -
    - {["yes", "no", "abstain"].map((j) => ( -
    - toggleArrayFilter("judgement", j)} - /> - -
    - ))} -
    -
    + + {(filterOptions?.statuses || []).map((status) => ( + + toggleArrayFilter("status", status.toLowerCase()) + } + /> + ))} + - {/* Category */} -
    - -
    - {(filterOptions?.categories || []).map((category) => ( -
    - - toggleArrayFilter("category", category) - } - /> - -
    - ))} -
    -
    + + {(filterOptions?.categories || []).map((category) => ( + toggleArrayFilter("category", category)} + /> + ))} + - {/* Sponsor Party */} -
    - -
    - {(filterOptions?.parties || []).map((party) => ( -
    - toggleArrayFilter("party", party)} - /> - -
    - ))} -
    -
    + + {(filterOptions?.parties || []).map((party) => ( + toggleArrayFilter("party", party)} + /> + ))} + - {/* Chamber */} -
    - -
    - {(filterOptions?.chambers || []).map((chamber) => ( -
    - - toggleArrayFilter("chamber", chamber) - } - /> - -
    - ))} -
    -
    + + {(filterOptions?.chambers || []).map((chamber) => ( + toggleArrayFilter("chamber", chamber)} + /> + ))} + - {/* Date Range */} -
    - - 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 - - + + + + + +
    - +
    )} - +
    ); } 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(" ")} >