From 5545558b0cbe0c3adc180bb22d8573ef790b3cb3 Mon Sep 17 00:00:00 2001 From: Jon Staab Date: Mon, 8 Jun 2026 15:16:09 -0700 Subject: [PATCH] First pass at full implementation --- src/Layout.tsx | 227 +++++++++++++++--------- src/components/QuorumChat.tsx | 132 ++++++++++++++ src/components/QuorumDetail.tsx | 88 +++++++++ src/components/QuorumList.tsx | 59 ++++++ src/components/forms/DkgForms.tsx | 148 +++++++++++++++ src/components/forms/ResharingForms.tsx | 139 +++++++++++++++ src/components/forms/SigningForms.tsx | 84 +++++++++ src/components/tabs/QuorumLog.tsx | 65 +++++++ src/components/tabs/QuorumMembers.tsx | 49 +++++ src/index.css | 4 + src/store.ts | 27 ++- 11 files changed, 934 insertions(+), 88 deletions(-) create mode 100644 src/components/QuorumChat.tsx create mode 100644 src/components/QuorumDetail.tsx create mode 100644 src/components/QuorumList.tsx create mode 100644 src/components/forms/DkgForms.tsx create mode 100644 src/components/forms/ResharingForms.tsx create mode 100644 src/components/forms/SigningForms.tsx create mode 100644 src/components/tabs/QuorumLog.tsx create mode 100644 src/components/tabs/QuorumMembers.tsx diff --git a/src/Layout.tsx b/src/Layout.tsx index a932b0c..9666713 100644 --- a/src/Layout.tsx +++ b/src/Layout.tsx @@ -1,37 +1,18 @@ -import { For, Show, createSignal, onMount } from "solid-js" -import { account, logout, view, setView, type View } from "./store" +import { createSignal, Show } from "solid-js" +import { account, logout, view, setView, activeQuorum } from "./store" +import QuorumList from "./components/QuorumList" +import QuorumDetail from "./components/QuorumDetail" +import QuorumChat from "./components/QuorumChat" +import { ProposeQuorum } from "./components/forms/DkgForms" +import { ProposeResharing } from "./components/forms/ResharingForms" +import { ProposeSign } from "./components/forms/SigningForms" -// Placeholder quorum list — will be replaced with real data -const MOCK_QUORUMS: { id: string; name: string }[] = [] - -function QuorumList() { - return ( - No quora yet

- }> - {q => ( - - )} -
- ) -} - -function SidebarContent() { +function SidebarContent(props: { onNew: () => void }) { const pubkey = () => account()?.pubkey ?? "" const shortKey = () => `${pubkey().slice(0, 8)}…${pubkey().slice(-4)}` return (
- {/* Inbox */}
- {/* Quorums */}
-
- - Quora - - -
- +
- {/* User profile + logout */}
@@ -86,64 +51,92 @@ function SidebarContent() { ) } -function MainContent() { - const v = view() - if (v === "inbox") { - return ( -
-

Inbox

-

No pending items.

-
- ) - } - return ( -
-

Quorum

-

Quorum detail coming soon.

-
- ) -} - export default function Layout() { const [drawerOpen, setDrawerOpen] = createSignal(false) + const [showProposeQuorum, setShowProposeQuorum] = createSignal(false) + const [showProposeResharing, setShowProposeResharing] = createSignal(false) + const [showProposeSign, setShowProposeSign] = createSignal(false) return (
{/* Desktop sidebar */} - {/* Mobile drawer overlay */} + {/* Mobile drawer backdrop */}
setDrawerOpen(false)} /> - - {/* Main area */} -
- {/* Mobile top bar */} -
+ {/* Mobile drawer — always mounted, slides in/out via transform */} + + + {/* Desktop: 3-column layout */} + + + {/* Mobile: main content area */} +
+
- + +
+

Inbox

+

+ Pending invitations and requests will appear here. +

+
+
+ + setShowProposeSign(true)} + onProposeResharing={() => setShowProposeResharing(true)} + /> +
+ + {/* Modals */} + +
setShowProposeQuorum(false)} + > +
e.stopPropagation()} + > + setShowProposeQuorum(false)} /> +
+
+
+ + +
setShowProposeResharing(false)} + > +
e.stopPropagation()} + > + setShowProposeResharing(false)} + /> +
+
+
+ + +
setShowProposeSign(false)} + > +
e.stopPropagation()} + > + setShowProposeSign(false)} + /> +
+
+
) } diff --git a/src/components/QuorumChat.tsx b/src/components/QuorumChat.tsx new file mode 100644 index 0000000..7f16697 --- /dev/null +++ b/src/components/QuorumChat.tsx @@ -0,0 +1,132 @@ +import { For, Show, createMemo } from "solid-js" +import { activeQuorum, dkgSessions, resharingSessions, signingSessions } from "../store" +import type { DkgSession, ResharingSession, SigningSession } from "../models" + +export default function QuorumChat() { + const pendingDkg = createMemo(() => + dkgSessions.filter(s => s.phase === "round1") + ) + + const pendingSigning = createMemo(() => { + const q = activeQuorum() + return signingSessions.filter(s => s.phase === "round1" && s.quorumPubkey === q?.quorumPubkey) + }) + + const inProgress = createMemo>(() => { + const result: Array<{ type: string; phase: string; id: string }> = [] + for (const s of dkgSessions) { + if (s.phase === "round2" || s.phase === "confirming") { + result.push({ type: "DKG", phase: s.phase, id: s.inviteId }) + } + } + for (const s of resharingSessions) { + if (s.phase === "round2" || s.phase === "confirming") { + result.push({ type: "Resharing", phase: s.phase, id: s.proposalId }) + } + } + for (const s of signingSessions) { + if (s.phase === "round2") { + result.push({ type: "Signing", phase: s.phase, id: s.requestId }) + } + } + return result + }) + + const isEmpty = createMemo(() => + pendingDkg().length === 0 && pendingSigning().length === 0 && inProgress().length === 0 + ) + + return ( +
+ +
+ No active sessions. +
+
+ + 0 || pendingSigning().length > 0}> +
+ + Needs Action + + + {(session: DkgSession) => ( +
+
+ + DKG Invite + + + {session.members.length} members · threshold {session.threshold} + +
+
+ + +
+
+ )} +
+ + {(session: SigningSession) => ( +
+
+ + Sign Request + + + {session.msgHex.slice(0, 16)}… + +
+
+ + +
+
+ )} +
+
+
+ + 0}> +
+ + In Progress + + + {(item) => ( +
+ + {item.type} + + + {item.phase} + +
+ )} +
+
+
+
+ ) +} diff --git a/src/components/QuorumDetail.tsx b/src/components/QuorumDetail.tsx new file mode 100644 index 0000000..d331f6a --- /dev/null +++ b/src/components/QuorumDetail.tsx @@ -0,0 +1,88 @@ +import { Show, Switch, Match } from "solid-js" +import { activeQuorum, view, setTab } from "../store" +import QuorumLog from "./tabs/QuorumLog" +import QuorumMembers from "./tabs/QuorumMembers" +import QuorumChat from "./QuorumChat" + +type Props = { + onProposeSign?: () => void + onProposeResharing?: () => void +} + +const TABS = ["log", "members", "chat"] as const +type Tab = (typeof TABS)[number] + +export default function QuorumDetail(props: Props) { + return ( + + Select a quorum from the sidebar +
+ } + > +
+
+
+
+ Quorum +
+
+ {activeQuorum()!.quorumPubkey} +
+
+
+ + +
+
+ +
+ {TABS.map(tab => ( + + ))} +
+ +
+ + + + + + + + +
+ +
+
+
+
+
+
+ ) +} diff --git a/src/components/QuorumList.tsx b/src/components/QuorumList.tsx new file mode 100644 index 0000000..9ac2efb --- /dev/null +++ b/src/components/QuorumList.tsx @@ -0,0 +1,59 @@ +import { For } from "solid-js" +import { quora, view, openQuorum } from "../store" +import type { QuorumRecord } from "../models" + +type Props = { + onNew?: () => void +} + +export default function QuorumList(props: Props) { + return ( + + ) +} diff --git a/src/components/forms/DkgForms.tsx b/src/components/forms/DkgForms.tsx new file mode 100644 index 0000000..2201b81 --- /dev/null +++ b/src/components/forms/DkgForms.tsx @@ -0,0 +1,148 @@ +import { createSignal } from "solid-js" +import { Show } from "solid-js" +import type { DkgSession } from "../../models" + +const inputClass = + "w-full px-3 py-2 text-sm border border-gray-200 dark:border-neutral-600 rounded-lg bg-white dark:bg-neutral-800 text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500" + +const primaryButtonClass = + "px-4 py-2 text-sm font-medium rounded-lg bg-blue-600 text-white hover:bg-blue-700 transition-colors" + +const secondaryButtonClass = + "px-4 py-2 text-sm font-medium rounded-lg border border-gray-200 dark:border-neutral-600 text-gray-700 dark:text-neutral-300 hover:bg-gray-50 dark:hover:bg-neutral-700 transition-colors" + +export function ProposeQuorum(props: { onClose: () => void }) { + const [pubkeysText, setPubkeysText] = createSignal("") + const [threshold, setThreshold] = createSignal(2) + const [error, setError] = createSignal("") + + function handleSubmit(e: Event) { + e.preventDefault() + const pubkeys = pubkeysText() + .split("\n") + .map(line => line.trim()) + .filter(line => line.length > 0) + + if (pubkeys.length < 2) { + setError("At least 2 members are required.") + return + } + + const t = threshold() + if (t < 1 || t > pubkeys.length) { + setError(`Threshold must be between 1 and ${pubkeys.length}.`) + return + } + + setError("") + console.log("ProposeQuorum", { pubkeys, threshold: t }) + props.onClose() + } + + return ( +
+

Create Quorum

+ +
+ +