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
28 changes: 28 additions & 0 deletions src/app/AppRoutes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ import Invision from "../pages/invision/Invision";
import Vortexopedia from "../pages/Vortexopedia";
import Factions from "../pages/factions/Factions";
import Faction from "../pages/factions/Faction";
import FactionChannel from "../pages/factions/FactionChannel";
import FactionInitiative from "../pages/factions/FactionInitiative";
import FactionThreadCreate from "../pages/factions/FactionThreadCreate";
import FactionInitiativeCreate from "../pages/factions/FactionInitiativeCreate";
import FactionCreate from "../pages/factions/FactionCreate";
import ProposalPP from "../pages/proposals/ProposalPP";
import ProposalChamber from "../pages/proposals/ProposalChamber";
Expand Down Expand Up @@ -49,6 +53,30 @@ const AppRoutes: React.FC = () => {
<Route path="factions" element={<Factions />} />
<Route path="factions/new" element={<FactionCreate />} />
<Route path="factions/:id" element={<Faction />} />
<Route
path="factions/:id/channels/:channelId"
element={<FactionChannel />}
/>
<Route
path="factions/:id/channels/:channelId/threads/:threadId"
element={<FactionChannel />}
/>
<Route
path="factions/:id/channels/:channelId/threads/new"
element={<FactionThreadCreate />}
/>
<Route
path="factions/:id/initiatives"
element={<FactionInitiative />}
/>
<Route
path="factions/:id/initiatives/:initiativeId"
element={<FactionInitiative />}
/>
<Route
path="factions/:id/initiatives/new"
element={<FactionInitiativeCreate />}
/>
<Route path="human-nodes" element={<HumanNodes />} />
<Route path="human-nodes/:id" element={<HumanNode />} />
<Route path="human-nodes/:id/history" element={<FullHistory />} />
Expand Down
23 changes: 17 additions & 6 deletions src/components/AddressInline.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useEffect, useState } from "react";
import { Link } from "react-router";
import { Check, Copy } from "lucide-react";
import { shortAddress } from "@/lib/profileUi";

Expand Down Expand Up @@ -38,12 +39,22 @@ export const AddressInline: React.FC<AddressInlineProps> = ({
<span
className={`inline-flex min-w-0 items-center gap-1 ${className ?? ""}`.trim()}
>
<span
title={address}
className={`min-w-0 truncate font-mono text-xs ${textClassName ?? ""}`.trim()}
>
{shortAddress(address, size)}
</span>
{address.trim().length > 0 ? (
<Link
to={`/app/human-nodes/${encodeURIComponent(address)}`}
title={address}
className={`min-w-0 truncate font-mono text-xs text-text hover:underline ${textClassName ?? ""}`.trim()}
>
{shortAddress(address, size)}
</Link>
) : (
<span
title={address}
className={`min-w-0 truncate font-mono text-xs ${textClassName ?? ""}`.trim()}
>
{shortAddress(address, size)}
</span>
)}
{showCopy ? (
<button
type="button"
Expand Down
25 changes: 18 additions & 7 deletions src/components/CardActionsRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type { ReactNode } from "react";
import { Link } from "react-router";

import { Button } from "@/components/primitives/button";
import { AddressInline } from "@/components/AddressInline";
import { cn } from "@/lib/utils";

type CardActionsRowProps = {
Expand Down Expand Up @@ -31,13 +32,23 @@ export function CardActionsRow({
className,
)}
>
{proposer && proposerId ? (
<Link
to={`/app/human-nodes/${proposerId}`}
className="min-w-0 text-sm font-semibold [overflow-wrap:anywhere] break-words text-primary"
>
Proposer: {proposer}
</Link>
{proposer ? (
<span className="inline-flex min-w-0 items-center gap-2 text-sm text-muted">
<span>Proposer:</span>
<AddressInline
address={proposer}
className="min-w-0"
textClassName="text-sm font-semibold text-text"
/>
{proposerId ? (
<Link
to={`/app/human-nodes/${proposerId}`}
className="text-xs font-semibold text-primary hover:underline"
>
Profile
</Link>
) : null}
</span>
) : (
<span className="text-sm text-muted"> </span>
)}
Expand Down
11 changes: 9 additions & 2 deletions src/components/ProposalPageHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
ProposalStageBar,
type ProposalStage,
} from "@/components/ProposalStageBar";
import { AddressInline } from "@/components/AddressInline";
import { StatTile } from "@/components/StatTile";

type ProposalPageHeaderProps = {
Expand Down Expand Up @@ -36,11 +37,17 @@ export function ProposalPageHeader({
/>
<StatTile
label="Proposer"
value={proposer}
value={
<AddressInline
address={proposer}
className="justify-center"
textClassName="text-base sm:text-lg"
/>
}
radius="2xl"
className="px-4 py-4"
labelClassName="text-[0.8rem]"
valueClassName="text-2xl"
valueClassName="text-lg"
/>
</div>
{children}
Expand Down
16 changes: 15 additions & 1 deletion src/components/ProposalSections.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
AttachmentList,
type AttachmentItem,
} from "@/components/AttachmentList";
import { AddressInline } from "@/components/AddressInline";
import { StatTile } from "@/components/StatTile";
import { Surface } from "@/components/Surface";
import { TitledSurface } from "@/components/TitledSurface";
Expand Down Expand Up @@ -229,6 +230,10 @@ type ProposalTimelineCardProps = {
items: ProposalTimelineItem[];
};

function isLikelyAddress(value: string): boolean {
return /^[a-z0-9]{6,}$/i.test(value) && value.length >= 20;
}

export function ProposalTimelineCard({ items }: ProposalTimelineCardProps) {
return (
<section className="space-y-3 text-sm text-text">
Expand All @@ -254,7 +259,16 @@ export function ProposalTimelineCard({ items }: ProposalTimelineCardProps) {
) : null}
{item.actor ? (
<p className="text-xs [overflow-wrap:anywhere] break-words text-muted">
Actor: {item.actor}
Actor:{" "}
{isLikelyAddress(item.actor) ? (
<AddressInline
address={item.actor}
className="inline-flex align-middle"
textClassName="text-xs text-muted"
/>
) : (
item.actor
)}
</p>
) : null}
</Surface>
Expand Down
27 changes: 27 additions & 0 deletions src/data/vortexopedia.ts
Original file line number Diff line number Diff line change
Expand Up @@ -613,6 +613,33 @@ export const vortexopediaTerms: VortexopediaTerm[] = [
source: "Voting, Delegation and Quorum",
updated: "2025-12-04",
},
{
ref: 22.1,
id: "upvote_floor",
name: "Upvote floor",
category: "governance",
short:
"Proposal-pool requirement: at least 10% of active governors must upvote for a proposal to advance to chamber vote.",
long: [
"Applied in every proposal pool, alongside the quorum of attention.",
"A proposal advances only if both are true: (1) ≥22% engagement (upvotes + downvotes) and (2) ≥10% upvotes.",
"Upvote floor prevents proposals from advancing with only downvotes/negative attention.",
],
tags: ["pool", "quorum", "upvote", "governance"],
related: ["quorum_of_attention", "proposal_pools"],
examples: [
"With 100 active governors, a proposal needs at least 10 upvotes to move to chamber vote.",
],
stages: ["pool"],
links: [
{
label: "Docs",
url: "https://gitbook.humanode.io/vortex-1.0",
},
],
source: "Voting, Delegation and Quorum",
updated: "2026-02-20",
},
{
ref: 23,
id: "quorum_of_vote",
Expand Down
67 changes: 64 additions & 3 deletions src/lib/apiClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -709,9 +709,13 @@ export async function apiFactionCreate(input: {
description: input.description,
...(input.focus ? { focus: input.focus } : {}),
...(input.visibility ? { visibility: input.visibility } : {}),
...(input.goals ? { goals: input.goals } : {}),
...(input.tags ? { tags: input.tags } : {}),
...(input.cofounders ? { cofounders: input.cofounders } : {}),
...(input.goals && input.goals.length > 0
? { goals: input.goals }
: {}),
...(input.tags && input.tags.length > 0 ? { tags: input.tags } : {}),
...(input.cofounders && input.cofounders.length > 0
? { cofounders: input.cofounders }
: {}),
},
idempotencyKey: input.idempotencyKey,
},
Expand Down Expand Up @@ -1069,6 +1073,63 @@ export async function apiFactionThreadTransition(input: {
);
}

export async function apiFactionThreadDelete(input: {
factionId: string;
threadId: string;
idempotencyKey?: string;
}): Promise<{
ok: true;
type: "faction.thread.delete";
factionId: string;
threadId: string;
deleted: true;
}> {
return await apiPost(
"/api/command",
{
type: "faction.thread.delete",
payload: {
factionId: input.factionId,
threadId: input.threadId,
},
idempotencyKey: input.idempotencyKey,
},
input.idempotencyKey
? { headers: { "idempotency-key": input.idempotencyKey } }
: undefined,
);
}

export async function apiFactionThreadReplyDelete(input: {
factionId: string;
threadId: string;
messageId: string;
idempotencyKey?: string;
}): Promise<{
ok: true;
type: "faction.thread.reply.delete";
factionId: string;
threadId: string;
messageId: string;
deleted: true;
}> {
return await apiPost(
"/api/command",
{
type: "faction.thread.reply.delete",
payload: {
factionId: input.factionId,
threadId: input.threadId,
messageId: input.messageId,
},
idempotencyKey: input.idempotencyKey,
},
input.idempotencyKey
? { headers: { "idempotency-key": input.idempotencyKey } }
: undefined,
);
}

export async function apiFactionInitiativeCreate(input: {
factionId: string;
title: string;
Expand Down
6 changes: 3 additions & 3 deletions src/lib/dtoParsers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,8 @@ export function computeChamberMetrics(chambers: ChamberDto[]) {
return sum + acm;
}, 0);
// Governors can be members of multiple chambers; use the largest chamber roster
// as a stable approximation of global active governors for the summary tile.
const activeGovernors = chambers.reduce((max, chamber) => {
// as a stable approximation of global governors for the summary tile.
const governors = chambers.reduce((max, chamber) => {
const { governors } = getChamberNumericStats(chamber);
return Math.max(max, governors);
}, 0);
Expand All @@ -50,7 +50,7 @@ export function computeChamberMetrics(chambers: ChamberDto[]) {
);
return {
totalChambers: chambers.length,
activeGovernors,
governors,
totalAcm,
liveProposals,
};
Expand Down
2 changes: 1 addition & 1 deletion src/pages/MyGovernance.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ const governingStatusForProgress = (
return { label: "Stable", termId: "governing_status_stable" };
}

if (completed >= required + 2) {
if (completed >= required + 1) {
return { label: "Ahead", termId: "governing_status_ahead" };
}

Expand Down
Loading