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 rsbuild.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ const isTest = process.env.RSTEST === "1";

export default defineConfig({
plugins: [pluginReact()],
html: {
title: "Vortex Sim",
},
server: {
...(isTest ? { host: "127.0.0.1", port: 0 } : {}),
proxy: {
Expand Down
1 change: 1 addition & 0 deletions src/app/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ const ScrollToTopOnRouteChange: React.FC = () => {
const { pathname, search } = useLocation();

useEffect(() => {
document.title = "Vortex Sim";
window.scrollTo({ top: 0, left: 0, behavior: "auto" });
}, [pathname, search]);

Expand Down
10 changes: 10 additions & 0 deletions src/app/AppRoutes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import FactionInitiativeCreate from "../pages/factions/FactionInitiativeCreate";
import FactionCreate from "../pages/factions/FactionCreate";
import ProposalPP from "../pages/proposals/ProposalPP";
import ProposalChamber from "../pages/proposals/ProposalChamber";
import ProposalCitizenVeto from "../pages/proposals/ProposalCitizenVeto";
import ProposalChamberVeto from "../pages/proposals/ProposalChamberVeto";
import ProposalReferendum from "../pages/proposals/ProposalReferendum";
import ProposalFormation from "../pages/proposals/ProposalFormation";
import ProposalFinished from "../pages/proposals/ProposalFinished";
Expand Down Expand Up @@ -91,6 +93,14 @@ const AppRoutes: React.FC = () => {
<Route path="proposals/new" element={<ProposalCreation />} />
<Route path="proposals/:id/pp" element={<ProposalPP />} />
<Route path="proposals/:id/chamber" element={<ProposalChamber />} />
<Route
path="proposals/:id/citizen-veto"
element={<ProposalCitizenVeto />}
/>
<Route
path="proposals/:id/chamber-veto"
element={<ProposalChamberVeto />}
/>
<Route
path="proposals/:id/referendum"
element={<ProposalReferendum />}
Expand Down
1 change: 1 addition & 0 deletions src/components/AttachmentList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export function AttachmentList({
title = "Attachments",
className,
}: AttachmentListProps) {
if (items.length === 0) return null;
return (
<Surface
variant="panelAlt"
Expand Down
99 changes: 59 additions & 40 deletions src/components/ProposalSections.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,20 +31,21 @@ export type ProposalMilestoneDetail = {
desc: string;
};

export type ProposalInvisionInsight = {
role: string;
bullets: string[];
};

export type ProposalTimelineItem = {
id: string;
timestamp: string;
title: string;
detail?: string;
actor?: string;
snapshot?: {
fromStage: "pool" | "vote" | "build";
toStage: "vote" | "build" | "passed" | "failed";
fromStage: "pool" | "vote" | "citizen_veto" | "chamber_veto" | "build";
toStage:
| "vote"
| "citizen_veto"
| "chamber_veto"
| "build"
| "passed"
| "failed";
reason?: string;
milestoneIndex?: number | null;
metrics: Array<{ label: string; value: string }>;
Expand All @@ -58,23 +59,51 @@ type ProposalSummaryCardProps = {
executionPlan: string[];
budgetScope: string;
attachments: AttachmentItem[];
showExecutionPlan?: boolean;
showBudgetScope?: boolean;
};

function canonicalizeProposalText(value: string): string {
return value
.toLowerCase()
.replace(/\s+/g, " ")
.trim()
.replace(/^[^a-z0-9]+/i, "")
.replace(/\b(the|a|an)\b/gi, " ")
.replace(/\s+/g, " ")
.replace(/[.!?]+$/g, "")
.trim();
}

export function ProposalSummaryCard({
summary,
stats,
overview,
executionPlan,
budgetScope,
attachments,
showExecutionPlan,
showBudgetScope,
}: ProposalSummaryCardProps) {
const normalizedSummary = summary.replace(/\s+/g, " ").trim();
const normalizedOverview = overview.replace(/\s+/g, " ").trim();
const showSummary = normalizedSummary !== normalizedOverview;
const normalizedBudgetScope = budgetScope.replace(/\s+/g, " ").trim();
const canonicalSummary = canonicalizeProposalText(normalizedSummary);
const canonicalOverview = canonicalizeProposalText(normalizedOverview);
const showSummary =
canonicalSummary.length > 0 &&
canonicalOverview.length > 0 &&
canonicalSummary !== canonicalOverview;
const showSummaryHeader = showSummary || stats.length > 0;
const renderExecutionPlan =
showExecutionPlan ?? executionPlan.some((item) => item.trim().length > 0);
const renderBudgetScope = showBudgetScope ?? normalizedBudgetScope.length > 0;

return (
<section className="space-y-3 text-sm text-muted">
<h2 className="text-lg font-semibold text-text">Summary</h2>
{showSummaryHeader ? (
<h2 className="text-lg font-semibold text-text">Summary</h2>
) : null}
{showSummary && <p>{summary}</p>}
{stats.length > 0 && (
<div className="grid gap-2 text-sm text-text sm:grid-cols-2 lg:grid-cols-4">
Expand All @@ -92,16 +121,20 @@ export function ProposalSummaryCard({
<TitledSurface title="Proposal overview">
<p className="text-sm leading-relaxed text-muted">{overview}</p>
</TitledSurface>
<TitledSurface title="Execution plan">
<ul className="list-disc space-y-1 pl-5 text-sm text-muted">
{executionPlan.map((item) => (
<li key={item}>{item}</li>
))}
</ul>
</TitledSurface>
<TitledSurface title="Budget & scope">
<p className="text-sm text-muted">{budgetScope}</p>
</TitledSurface>
{renderExecutionPlan ? (
<TitledSurface title="Execution plan">
<ul className="list-disc space-y-1 pl-5 text-sm text-muted">
{executionPlan.map((item) => (
<li key={item}>{item}</li>
))}
</ul>
</TitledSurface>
) : null}
{renderBudgetScope ? (
<TitledSurface title="Budget & scope">
<p className="text-sm text-muted">{budgetScope}</p>
</TitledSurface>
) : null}
<AttachmentList items={attachments} />
</div>
</section>
Expand Down Expand Up @@ -214,26 +247,6 @@ export function ProposalTeamMilestonesCard({
);
}

type ProposalInvisionInsightCardProps = {
insight: ProposalInvisionInsight;
};

export function ProposalInvisionInsightCard({
insight,
}: ProposalInvisionInsightCardProps) {
return (
<section className="space-y-3 text-sm text-text">
<h2 className="text-lg font-semibold text-text">Invision insight</h2>
<p className="text-sm font-semibold text-text">Role: {insight.role}</p>
<ul className="list-disc space-y-2 pl-5 text-muted">
{insight.bullets.map((item) => (
<li key={item}>{item}</li>
))}
</ul>
</section>
);
}

type ProposalTimelineCardProps = {
items: ProposalTimelineItem[];
proposalId?: string;
Expand All @@ -245,12 +258,18 @@ function isLikelyAddress(value: string): boolean {

function snapshotStageHref(
proposalId: string,
stage: "pool" | "vote" | "build",
stage: "pool" | "vote" | "citizen_veto" | "chamber_veto" | "build",
): string | null {
if (stage === "pool")
return `/app/proposals/${proposalId}/pp?snapshotStage=pool`;
if (stage === "vote")
return `/app/proposals/${proposalId}/chamber?snapshotStage=vote`;
if (stage === "citizen_veto") {
return `/app/proposals/${proposalId}/citizen-veto?snapshotStage=citizen_veto`;
}
if (stage === "chamber_veto") {
return `/app/proposals/${proposalId}/chamber-veto?snapshotStage=chamber_veto`;
}
// `build` stage can become unavailable after terminal transition.
return null;
}
Expand Down
18 changes: 13 additions & 5 deletions src/components/ProposalStageBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ export type ProposalStage =
| "draft"
| "pool"
| "vote"
| "citizen_veto"
| "chamber_veto"
| "build"
| "passed"
| "failed";
Expand Down Expand Up @@ -36,6 +38,8 @@ export const ProposalStageBar: React.FC<ProposalStageBarProps> = ({
label: "Chamber vote",
render: <HintLabel termId="chamber_vote">Chamber vote</HintLabel>,
},
{ key: "citizen_veto", label: "Citizen veto" },
{ key: "chamber_veto", label: "Chamber veto" },
{
key: "build",
label: "Formation",
Expand All @@ -62,11 +66,15 @@ export const ProposalStageBar: React.FC<ProposalStageBarProps> = ({
? "bg-primary text-[var(--primary-foreground)]"
: stage.key === "vote"
? "bg-[var(--accent)] text-[var(--accent-foreground)]"
: stage.key === "build"
? "bg-[var(--accent-warm)] text-[var(--text)]"
: stage.key === "passed"
? "bg-[color:var(--ok)]/20 text-[color:var(--ok)]"
: "bg-[color:var(--danger)]/12 text-[color:var(--danger)]";
: stage.key === "citizen_veto"
? "bg-[color:var(--danger)]/14 text-[color:var(--danger)]"
: stage.key === "chamber_veto"
? "bg-[color:var(--danger)]/18 text-[color:var(--danger)] ring-1 ring-[color:var(--danger)]/25"
: stage.key === "build"
? "bg-[var(--accent-warm)] text-[var(--text)]"
: 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}
Expand Down
3 changes: 3 additions & 0 deletions src/components/StageChip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ import {
const chipClasses: Record<StageChipKind, string> = {
proposal_pool: "bg-[color:var(--accent-warm)]/15 text-[var(--accent-warm)]",
chamber_vote: "bg-[color:var(--accent)]/15 text-[var(--accent)]",
citizen_veto: "bg-[color:var(--danger)]/12 text-[color:var(--danger)]",
chamber_veto:
"bg-[color:var(--danger)]/16 text-[color:var(--danger)] ring-1 ring-[color:var(--danger)]/20",
formation: "bg-[color:var(--primary)]/12 text-primary",
passed: "bg-[color:var(--ok)]/20 text-[color:var(--ok)]",
failed: "bg-[color:var(--danger)]/12 text-[color:var(--danger)]",
Expand Down
Loading
Loading