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
12 changes: 11 additions & 1 deletion example-apps/dashnote/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { HowItWorks } from "./components/HowItWorks";
import { LoginModal } from "./components/LoginModal";
import { NotesWorkspace } from "./components/NotesWorkspace";
import { OperationResultNotice } from "./components/OperationResultNotice";
import { SettingsPanel } from "./components/SettingsPanel";
import type { TopTab } from "./components/Tabs";
import { useSession } from "./session/useSession";

Expand All @@ -20,6 +21,11 @@ const screenCopy: Record<TopTab, { title: string; subtitle: string }> = {
subtitle:
"See how the note contract, mutation helpers, and notebook UI line up with the tutorials.",
},
settings: {
title: "Settings",
subtitle:
"Manage your identity, contract, and local data for this browser.",
},
};

function App() {
Expand Down Expand Up @@ -86,9 +92,13 @@ function App() {
)}

{tab === "notes" && (
<NotesWorkspace onOpenSettings={() => setLoginOpen(true)} />
<NotesWorkspace
onOpenLogin={() => setLoginOpen(true)}
onOpenSettings={() => setTab("settings")}
/>
)}
{tab === "how-it-works" && <HowItWorks />}
{tab === "settings" && <SettingsPanel />}
</div>
</AppShell>

Expand Down
13 changes: 13 additions & 0 deletions example-apps/dashnote/src/components/AppShell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,15 @@ export function AppShell({
closeDrawer();
}}
/>
<NavButton
label="Settings"
glyph="⚙"
active={tab === "settings"}
onClick={() => {
onTabChange("settings");
closeDrawer();
}}
/>
{status !== "authenticated" && (
<NavButton
label="Login"
Expand Down Expand Up @@ -197,6 +206,10 @@ export function AppShell({
onLoginOpen();
closeDrawer();
}}
onOpenSettings={() => {
onTabChange("settings");
closeDrawer();
}}
/>
</div>
</aside>
Expand Down
197 changes: 147 additions & 50 deletions example-apps/dashnote/src/components/IdentityCard.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
import { useEffect, useRef, useState } from "react";

import type { SessionStatus } from "../session/SessionContext";
import { truncateId } from "../lib/format";
import { useSession } from "../session/useSession";

interface IdentityCardProps {
status: SessionStatus;
identityId: string | null;
dpnsName: string | null;
contractId: string | null;
onLoginClick: () => void;
onOpenSettings: () => void;
}

function avatarGradient(seed: string | null): string {
Expand All @@ -26,10 +30,36 @@ export function IdentityCard({
dpnsName,
contractId,
onLoginClick,
onOpenSettings,
}: IdentityCardProps) {
const session = useSession();
const isAuthed = status === "authenticated";
const isBrowsing = status === "browsing";
const isConnected = status === "readonly" || isAuthed || isBrowsing;
const isReadonly = status === "readonly";
const isConnected = isReadonly || isAuthed || isBrowsing;
const hasIdentity = isAuthed || isBrowsing;
const [menuOpen, setMenuOpen] = useState(false);
const containerRef = useRef<HTMLDivElement>(null);

useEffect(() => {
if (!menuOpen) return;
const onPointer = (event: MouseEvent | TouchEvent) => {
if (!containerRef.current) return;
if (containerRef.current.contains(event.target as Node)) return;
setMenuOpen(false);
};
const onKey = (event: KeyboardEvent) => {
if (event.key === "Escape") setMenuOpen(false);
};
document.addEventListener("mousedown", onPointer);
document.addEventListener("touchstart", onPointer);
document.addEventListener("keydown", onKey);
return () => {
document.removeEventListener("mousedown", onPointer);
document.removeEventListener("touchstart", onPointer);
document.removeEventListener("keydown", onKey);
};
}, [menuOpen]);

if (!isConnected) {
return (
Expand Down Expand Up @@ -63,59 +93,126 @@ export function IdentityCard({
);
}

// Read-only mode has nothing to put in a menu (no identity → no Settings
// target, no Switch identity, no Log out), so the card goes straight to
// the login modal on click — matching the pre-menu behavior.
if (isReadonly) {
return (
<button
type="button"
onClick={onLoginClick}
className="group w-full border-t border-line pt-3.5 text-left"
>
<div className="flex items-center justify-between">
<span className="text-[10px] font-semibold uppercase tracking-[0.12em] text-ink-4">
Connected
</span>
<span className="text-[10px] text-ink-4 opacity-0 transition-opacity group-hover:opacity-100">
Sign in
</span>
</div>
<div className="mt-2.5 flex items-center gap-1.5">
<span className="conn-dot connected" />
<span className="font-mono text-[10.5px] text-ink-3">Connected</span>
</div>
</button>
);
}

return (
<button
type="button"
onClick={onLoginClick}
className="group w-full border-t border-line pt-3.5 text-left"
>
{(isAuthed || isBrowsing) && (
<>
<div className="mb-0.5 flex items-center justify-between">
<span className="text-[10px] font-semibold uppercase tracking-[0.12em] text-ink-4">
{isAuthed ? "Signed in" : "Read-only"}
</span>
<span className="text-[10px] text-ink-4 opacity-0 transition-opacity group-hover:opacity-100">
{isAuthed ? "Settings" : "Sign in"}
</span>
</div>
<div className="flex items-center gap-2">
<div
className="h-5 w-5 shrink-0 rounded-full"
style={{ background: avatarGradient(identityId) }}
/>
<div className="min-w-0">
<div className="truncate text-[12px] font-medium text-ink transition-colors group-hover:text-accent">
{dpnsName
? `@${dpnsName}`
: identityId
<div ref={containerRef} className="relative">
<button
type="button"
onClick={() => setMenuOpen((v) => !v)}
aria-haspopup="menu"
aria-expanded={menuOpen}
className="group w-full border-t border-line pt-3.5 text-left"
>
{hasIdentity && (
<>
<div className="mb-0.5 flex items-center justify-between">
<span className="text-[10px] font-semibold uppercase tracking-[0.12em] text-ink-4">
{isAuthed ? "Signed in" : "Read-only"}
</span>
<span className="text-[10px] text-ink-4 opacity-0 transition-opacity group-hover:opacity-100">
Menu
</span>
</div>
<div className="flex items-center gap-2">
<div
className="h-5 w-5 shrink-0 rounded-full"
style={{ background: avatarGradient(identityId) }}
/>
<div className="min-w-0">
<div className="truncate text-[12px] font-medium text-ink transition-colors group-hover:text-accent">
{dpnsName
? `@${dpnsName}`
: identityId
? truncateId(identityId, 6)
: "Identity"}
</div>
<div className="truncate font-mono text-[10px] text-ink-4">
{dpnsName && identityId
? truncateId(identityId, 6)
: "Identity"}
</div>
<div className="truncate font-mono text-[10px] text-ink-4">
{dpnsName && identityId
? truncateId(identityId, 6)
: contractId
? `contract ${truncateId(contractId, 6)}`
: "No contract"}
: contractId
? `contract ${truncateId(contractId, 6)}`
: "No contract"}
</div>
</div>
</div>
</div>
</>
)}
</>
)}

<div
className={`flex items-center gap-1.5 ${isAuthed || isBrowsing ? "mt-2.5" : ""}`}
>
<span className="conn-dot connected" />
<span className="font-mono text-[10.5px] text-ink-3">
{isAuthed
? "Authenticated"
: isBrowsing
? "Browsing (read-only)"
: "Connected"}
</span>
</div>
</button>
<div className="mt-2.5 flex items-center gap-1.5">
<span className="conn-dot connected" />
<span className="font-mono text-[10.5px] text-ink-3">
{isAuthed ? "Authenticated" : "Browsing (read-only)"}
</span>
</div>
</button>

{menuOpen && (
<div
role="menu"
className="absolute bottom-full left-0 right-0 z-40 mb-2 flex flex-col gap-0.5 rounded-md border border-line bg-surface p-1 shadow-[0_12px_40px_-20px_rgba(0,0,0,0.6)]"
>
<button
type="button"
role="menuitem"
onClick={() => {
setMenuOpen(false);
onOpenSettings();
}}
className="rounded-md px-2 py-1.5 text-left text-[12px] font-medium text-ink-2 transition hover:bg-surface-2 hover:text-ink"
>
Settings
</button>
<button
type="button"
role="menuitem"
onClick={() => {
setMenuOpen(false);
onLoginClick();
}}
className="rounded-md px-2 py-1.5 text-left text-[12px] font-medium text-ink-2 transition hover:bg-surface-2 hover:text-ink"
>
Switch identity
</button>
{isAuthed && (
<button
type="button"
role="menuitem"
onClick={() => {
setMenuOpen(false);
session.logout();
}}
className="rounded-md px-2 py-1.5 text-left text-[12px] font-medium text-ink-2 transition hover:bg-surface-2 hover:text-ink"
>
Log out
</button>
)}
</div>
)}
</div>
);
}
Loading