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
3 changes: 3 additions & 0 deletions src/components/ProposalPageHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ type ProposalPageHeaderProps = {
showFormationStage?: boolean;
chamber: string;
proposer: string;
stageLinks?: Partial<Record<ProposalStage, string>>;
children?: ReactNode;
};

Expand All @@ -22,6 +23,7 @@ export function ProposalPageHeader({
showFormationStage = true,
chamber,
proposer,
stageLinks,
children,
}: ProposalPageHeaderProps) {
return (
Expand All @@ -30,6 +32,7 @@ export function ProposalPageHeader({
<ProposalStageBar
current={stage}
showFormationStage={showFormationStage}
stageLinks={stageLinks}
/>
<div className="grid gap-3 sm:grid-cols-2">
<StatTile
Expand Down
30 changes: 19 additions & 11 deletions src/components/ProposalStageBar.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React from "react";
import { Link } from "react-router";
import { HintLabel } from "@/components/Hint";

export type ProposalStage =
Expand All @@ -15,12 +16,14 @@ type ProposalStageBarProps = {
current: ProposalStage;
showFormationStage?: boolean;
className?: string;
stageLinks?: Partial<Record<ProposalStage, string>>;
};

export const ProposalStageBar: React.FC<ProposalStageBarProps> = ({
current,
showFormationStage = true,
className,
stageLinks,
}) => {
const allStages: {
key: ProposalStage;
Expand Down Expand Up @@ -59,6 +62,7 @@ export const ProposalStageBar: React.FC<ProposalStageBarProps> = ({
>
{stages.map((stage) => {
const active = stage.key === current;
const href = stageLinks?.[stage.key];
const activeClasses =
stage.key === "draft"
? "bg-panel text-text border border-border shadow-[var(--shadow-control)] ring-1 ring-inset ring-[color:var(--glass-border)]"
Expand All @@ -75,17 +79,21 @@ export const ProposalStageBar: React.FC<ProposalStageBarProps> = ({
: stage.key === "passed"
? "bg-[color:var(--ok)]/20 text-[color:var(--ok)]"
: "bg-[color:var(--danger)]/12 text-[color:var(--danger)]";
return (
<div
key={stage.key}
className={[
"min-w-0 basis-[calc(50%-0.25rem)] rounded-full px-3 py-2 text-center text-xs leading-tight font-semibold transition sm:flex-1 sm:basis-0",
active
? activeClasses
: "border border-border bg-panel-alt [background-image:var(--card-grad)] bg-cover bg-no-repeat text-muted",
].join(" ")}
>
{stage.render ?? stage.label}
const className = [
"min-w-0 basis-[calc(50%-0.25rem)] rounded-full px-3 py-2 text-center text-xs leading-tight font-semibold transition sm:flex-1 sm:basis-0",
active
? activeClasses
: "border border-border bg-panel-alt [background-image:var(--card-grad)] bg-cover bg-no-repeat text-muted",
href ? "hover:border-border-strong hover:text-text" : "",
].join(" ");
const content = stage.render ?? stage.label;
return href ? (
<Link key={stage.key} to={href} className={className}>
{content}
</Link>
) : (
<div key={stage.key} className={className}>
{content}
</div>
);
})}
Expand Down
100 changes: 15 additions & 85 deletions src/pages/MyGovernance.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import { Button } from "@/components/primitives/button";
import { Input } from "@/components/primitives/input";
import { AddressInline } from "@/components/AddressInline";
import { PipelineList } from "@/components/PipelineList";
import { StageChip } from "@/components/StageChip";
import { StatGrid, makeChamberStats } from "@/components/StatGrid";
import { Surface } from "@/components/Surface";
import { PageHint } from "@/components/PageHint";
Expand All @@ -28,7 +27,6 @@ import {
apiLegitimacyObjectSet,
apiCmMe,
apiMyGovernance,
apiProposals,
} from "@/lib/apiClient";
import { formatLoadError } from "@/lib/errorFormatting";
import { toTimestampMs } from "@/lib/dateTime";
Expand All @@ -37,7 +35,6 @@ import type {
CmSummaryDto,
GetClockResponse,
GetMyGovernanceResponse,
ProposalListItemDto,
} from "@/types/api";
import { cn } from "@/lib/utils";

Expand Down Expand Up @@ -185,7 +182,6 @@ const MyGovernance: React.FC = () => {
const [delegationErrorByChamber, setDelegationErrorByChamber] = useState<
Record<string, string | null>
>({});
const [vetoWindows, setVetoWindows] = useState<ProposalListItemDto[]>([]);
const [nowMs, setNowMs] = useState<number>(() => Date.now());

useEffect(() => {
Expand Down Expand Up @@ -232,39 +228,6 @@ const MyGovernance: React.FC = () => {
};
}, []);

useEffect(() => {
if (!gov) {
setVetoWindows([]);
return;
}
let active = true;
const currentTierLabel = gov.tier?.tier ?? "Nominee";
(async () => {
try {
const [citizenResult, chamberResult] = await Promise.all([
currentTierLabel === "Citizen"
? apiProposals({ stage: "citizen_veto" })
: Promise.resolve({ items: [] as ProposalListItemDto[] }),
currentTierLabel !== "Nominee"
? apiProposals({ stage: "chamber_veto" })
: Promise.resolve({ items: [] as ProposalListItemDto[] }),
]);
if (!active) return;
setVetoWindows(
[...citizenResult.items, ...chamberResult.items].sort(
(a, b) => toTimestampMs(b.date, -1) - toTimestampMs(a.date, -1),
),
);
} catch {
if (!active) return;
setVetoWindows([]);
}
})();
return () => {
active = false;
};
}, [gov]);

const eraActivity = gov?.eraActivity;
const timeLeftValue = useMemo(() => {
const targetMs = clock?.nextEraAt
Expand Down Expand Up @@ -727,7 +690,7 @@ const MyGovernance: React.FC = () => {

<Card>
<CardHeader className="pb-2">
<CardTitle>Veto windows</CardTitle>
<CardTitle>Veto process</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<Surface
Expand All @@ -736,54 +699,21 @@ const MyGovernance: React.FC = () => {
shadow="tile"
className="px-4 py-3 text-sm text-muted"
>
Snapshot eligibility is finalized on each veto proposal page. This
section highlights currently open Citizen and Chamber Veto windows
you may want to inspect.
Veto happens on the chamber vote page itself. Citizens and chambers
can cast veto votes during the chamber vote window, and if the
ordinary vote passes, a 24h veto countdown remains open on that same
page.
</Surface>
<Surface
variant="panelAlt"
radius="2xl"
shadow="tile"
className="px-4 py-3 text-sm text-muted"
>
If a veto succeeds, the proposal returns to reconsideration draft
and the proposer can resubmit it directly into chamber vote without
going through proposal pool again.
</Surface>
{vetoWindows.length === 0 ? (
<Surface
variant="panelAlt"
radius="2xl"
shadow="tile"
className="px-4 py-3 text-sm text-muted"
>
No live veto windows right now.
</Surface>
) : (
<div className="grid gap-3">
{vetoWindows.map((proposal) => (
<Surface
key={proposal.id}
variant="panelAlt"
radius="2xl"
shadow="tile"
className="flex flex-col gap-3 px-4 py-4"
>
<div className="flex flex-col gap-2 sm:flex-row sm:items-start sm:justify-between">
<div className="space-y-1">
<div className="flex flex-wrap items-center gap-2">
<StageChip stage={proposal.stage} />
<span className="text-xs text-muted">
{proposal.chamber}
</span>
</div>
<p className="text-sm font-semibold text-text">
{proposal.title}
</p>
<p className="text-xs text-muted">{proposal.summary}</p>
</div>
<Button asChild size="sm">
<Link
to={proposal.href ?? `/app/proposals/${proposal.id}/pp`}
>
Open
</Link>
</Button>
</div>
</Surface>
))}
</div>
)}
</CardContent>
</Card>

Expand Down
68 changes: 68 additions & 0 deletions src/pages/proposals/CitizenVetoActions.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { VoteButton } from "@/components/VoteButton";
import type { ChamberProposalPageDto } from "@/types/api";

type CitizenVetoActionsProps = {
citizenVeto: ChamberProposalPageDto["citizenVeto"];
viewerIsProposer: boolean;
windowOpen: boolean;
submittingKey: string | null;
onVote: () => void;
};

export const CitizenVetoActions: React.FC<CitizenVetoActionsProps> = ({
citizenVeto,
viewerIsProposer,
windowOpen,
submittingKey,
onVote,
}) => {
const currentVote = citizenVeto.viewer.currentVote;
const vetoRecorded = currentVote === "veto";
const tone = currentVote === "keep" ? "neutral" : "destructive";
const disabled =
Boolean(submittingKey) ||
viewerIsProposer ||
!windowOpen ||
!citizenVeto.available ||
!citizenVeto.viewer.eligible ||
currentVote !== null;

return (
<div className="flex flex-wrap items-center justify-center gap-3">
<VoteButton
tone={tone}
label={
submittingKey
? "Casting veto..."
: currentVote === "veto"
? "Citizen veto cast"
: currentVote === "keep"
? "Citizen keep recorded"
: "Citizen veto"
}
className={
vetoRecorded
? "bg-[var(--destructive)] text-[var(--destructive-foreground)] hover:bg-[var(--destructive)] hover:text-[var(--destructive-foreground)]"
: undefined
}
disabled={disabled}
title={
viewerIsProposer
? "You cannot veto your own proposal."
: currentVote === "veto"
? "Your citizen veto is already recorded."
: currentVote === "keep"
? "Your citizen keep vote is already recorded. Use the Citizen Veto tab to change it."
: !windowOpen
? "Veto window ended."
: !citizenVeto.available
? "Citizen veto is exhausted for this decision."
: !citizenVeto.viewer.eligible
? "Only Citizens captured when this veto window opened can vote here."
: undefined
}
onClick={onVote}
/>
</div>
);
};
Loading
Loading