Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
14 changes: 4 additions & 10 deletions src/app/toronto/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { breakdown, msUntil, periodTiming } from "@/lib/elections/dates";
import { isDaylight } from "@/lib/daylight";
import { ELECTION } from "./vote/2026/data";
import { STAGE_ONE } from "./vote/survey-questions/questions";
import { SURVEY_PATH } from "./vote/survey-questions/path";
import { WARD_GEO, WARD_SHAPES } from "./vote/2026/wardGeo";
import {
ELECTION_DAY,
Expand Down Expand Up @@ -304,17 +305,10 @@ function ElectionCardsSection() {
</ElectionCard>

<ElectionCard
/* Points at the signup rather than the survey page for now: the
questions are written but the answers aren't in, so the card
sells being told when they are. Swap this back to
href={SURVEY_PATH} at launch.

Pitched as the reader's own comparison, not as our research: the
/* Pitched as the reader's own comparison, not as our research: the
questions only matter to a voter because answering them yourself
is what sorts the field. This is the one card whose action is a
signup rather than a link, so it is also the one that needs a
line of copy to say what it does. */
modalHeadline="Be first to compare your views with the candidates’"
is what sorts the field. */
href={SURVEY_PATH}
title="Compare your views to the candidates"
cta="Compare your views"
className="bg-bg-alt text-dark"
Expand Down
3 changes: 1 addition & 2 deletions src/app/toronto/vote/2026/wards/[ward]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ export default async function WardDetailPage({
const candidateKeys = new Set(
data.councilRaces.flatMap((race) => race.candidates.map((c) => c.key)),
);
const { answers: surveyAnswers, shape: surveyShape } = await rosterSurvey(
const { answers: surveyAnswers } = await rosterSurvey(
ELECTION.slug,
candidateKeys,
);
Expand All @@ -57,7 +57,6 @@ export default async function WardDetailPage({
data={data}
nominationCloseLabel={view.nominationCloseLabel}
surveyAnswers={surveyAnswers}
surveyShape={surveyShape}
profile={
wardProfile(
data.ward.n,
Expand Down
37 changes: 20 additions & 17 deletions src/components/elections/QuestionSplit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { rollCall } from "@/lib/elections/candidate-answers";
import { EMPTY, optionColors } from "@/lib/elections/option-colors";
import { QuestionSplitFigure } from "./QuestionSplitFigure";
import type { SplitSlice } from "./QuestionSplitFigure";
import type { Seat } from "./QuestionRollCall";
import type { ComparedQuestion } from "@/lib/elections/candidate-answers";

/* One question as a card, read as a share of the field rather than a roll call.
Expand Down Expand Up @@ -36,11 +35,11 @@ import type { ComparedQuestion } from "@/lib/elections/candidate-answers";
* Behind the legend rows, one option at a time — see QuestionSplitFigure.
* Hidden, they stop crowding out the split; reachable, the reader can still
* check who is in a segment, which is the difference between a chart and a
* chart you have to take on trust. Each name carries the seat it is running
* for, because a name on a city-wide page is only useful once the reader
* knows whether it is on their ballot. What they WROTE stays on the ward and
* mayoral pages: a note is a paragraph, and a panel of thirty paragraphs is
* the page this card was drawn to get away from.
* chart you have to take on trust. Names alone — the seat each is running
* for rode beside them once and cost more width than it paid for; see
* `named`. What they WROTE stays on the ward and mayoral pages: a note is a
* paragraph, and a panel of thirty paragraphs is the page this card was
* drawn to get away from.
*
* COLOUR
* The same ramps as everywhere else in the tracker (lib/elections/
Expand All @@ -52,29 +51,26 @@ import type { ComparedQuestion } from "@/lib/elections/candidate-answers";

export function QuestionSplit({
question,
seats,
headingId,
}: {
question: ComparedQuestion;
/** the seat each candidate is running for, keyed by candidate key — printed
* beside their name in the panel behind a segment. */
seats?: Record<string, Seat>;
/** the id the scroll rail scrolls to — see QuestionRollCall */
headingId?: string;
}) {
const { groups, verbatim } = rollCall(question);

const colors = optionColors(question.options.length, question.ordinal);

/* Name only. This card took a `seats` map once and printed "· Ward 17" or
"· For mayor" after every name in the panel, on the argument that a name
on a city-wide page is only useful once the reader knows whether it is on
their ballot. True, and still the wrong place for it: the suffix is
longer than most of the names it trails, it set the width of every column
in the panel, and the panel is a list of fifty the reader scans for one
they recognise. The ward pages are where a name is matched to a ballot. */
const named = (candidate: { key: string; name: string }) => ({
key: candidate.key,
name: candidate.name,
/* Mayoral candidates carry no label of their own — a ward number is the
thing that tells a reader whether a name is on their ballot, and "For
mayor" has to be spelled out rather than left blank beside it. */
seat: seats?.[candidate.key]
? (seats[candidate.key].label ?? "For mayor")
: undefined,
});

const slices: SplitSlice[] = groups.map((group) => ({
Expand Down Expand Up @@ -104,9 +100,16 @@ export function QuestionSplit({

return (
<article className="flex flex-col gap-4 border border-border-light p-6 md:p-7">
{/* Smaller than a card heading usually runs, and smaller than it was.
A question here is a line of up to ninety characters over a card
three to a row, so at 1.45rem it took three and four lines and the
page read as a stack of headlines with charts attached. The chart is
what the reader is scanning; the question is what tells them which
chart it is. Close to the roll-call card's own base size, so the two
kinds of card sit at one scale where a reader meets both. */}
<h3
id={headingId}
className="scroll-mt-24 font-sans font-medium leading-[1.2] tracking-[-0.025em] text-[1.45rem] text-dark text-pretty"
className="scroll-mt-24 font-sans font-medium leading-[1.2] tracking-[-0.025em] text-[1.25rem] text-dark text-pretty"
>
{question.question}
</h3>
Expand Down
Loading
Loading