Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
37b7241
refactor(mail): say each privacy fact once, and name the open thread
claude Sep 4, 2026
c1deadb
fix(mail): end the first-run dead end at a wallet, not a notice
claude Sep 4, 2026
32d69de
style(mail): give each record colour back its one meaning
claude Sep 4, 2026
433c357
fix(mail): give the composer a measure, and stop delete from flanking…
claude Sep 4, 2026
eb7937b
style(mail): make the sidebar one control set instead of four
claude Sep 4, 2026
d176514
refactor(mail): make a rail row say four different things
claude Sep 4, 2026
dbaae33
fix(mail): stop the reading pane from being squeezed, and phones from
claude Sep 4, 2026
fcfb848
feat(mail): keep reply in reach, and make first-run state its own case
claude Sep 4, 2026
cff4111
refactor(mail): put a record's own line back to one line
claude Sep 4, 2026
629cc2f
feat(mail): group amounts, and rank a card's warnings
claude Sep 4, 2026
c011947
fix(mail): stop Ignore from reading as an invoice's main action
claude Sep 4, 2026
d722be3
style(shell): make connecting the header's strongest control
claude Sep 4, 2026
d0b1dc7
style(shell): bring the wallet picker onto the desk
claude Sep 4, 2026
d1172e3
style(rfq): give the workspace a legible frame
claude Sep 4, 2026
848c141
fix(rfq): answer the disconnected desk once, with the step that exists
claude Sep 4, 2026
f65d039
fix(rfq): make the decision surfaces readable and honest
claude Sep 4, 2026
bcd85b4
fix(rfq): let a record say what the trade is before it says how exact…
claude Sep 4, 2026
df54d48
chore: drop a screenshot scratch script committed by mistake
claude Sep 4, 2026
0d7021e
fix(contacts): style the masthead the page was already asking for
claude Sep 4, 2026
8f063a2
test: fail the build when a CSS-module class does not exist
claude Sep 4, 2026
1943e01
style(funding): bring the readiness gate into the desk's language
claude Sep 4, 2026
fdf1865
feat(mail): keep opened state across reloads, and let a mailbox be se…
claude Sep 4, 2026
02964a8
refactor(rfq): put the ticket first, and stop splitting one set of re…
claude Sep 4, 2026
59006f6
feat(mail): name a counterparty once and see the name everywhere
claude Sep 4, 2026
861a128
refactor: say each standing caveat once, where it applies
claude Sep 4, 2026
76700eb
fix: answer one gate once, and put conversation actions on the conver…
claude Sep 4, 2026
13f9e8c
test(ui): follow the merged Records view, and open the recovery discl…
claude Sep 4, 2026
aea41e9
fix(rfq): let the briefing sit in the ticket, and make the ticket rea…
claude Sep 4, 2026
766b888
test(ui): match the compose recipient-count line
claude Sep 4, 2026
333d318
test(ui): match the mailbox row's spoken name for a settlement memo
claude Sep 4, 2026
6211991
test(ui): open the market board before reaching for its freshness tip
claude Sep 4, 2026
b7bb702
test(ui): the RFQ sub-nav is New and Records
claude Sep 4, 2026
5ae4237
test(ui): a cancelled record is read under the All scope
claude Sep 4, 2026
bbaedaa
fix(rfq): land a legacy #activity bookmark on the whole history
claude Sep 4, 2026
509d0d9
fix(rfq): say the reconciliation rule under the whole history too
claude Sep 4, 2026
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
27 changes: 18 additions & 9 deletions src/app/components/AppShell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,16 @@ export default function AppShell({ renderLocalnetTools }: AppShellProps) {
const mailActive = pathname.startsWith("/mail") || pathname === "/inbox";
const rfqActive = pathname === "/rfq" || pathname.startsWith("/rfq/");
const connected = walletMode === "privy" ? privyConnected : readyConnected;
const payActive = pathname === "/pay";
const moduleName = mailActive
? "MAILBOX"
: payActive
? "PAY"
: pathname === "/contacts"
? "COUNTERPARTIES"
: rfqActive
? "RFQ WORKSPACE"
: "APP20";

return (
<div
Expand All @@ -31,8 +41,11 @@ export default function AppShell({ renderLocalnetTools }: AppShellProps) {
className={`signal-dot ${connected ? "is-live" : ""}`}
aria-hidden="true"
/>
{connected ? "WALLET CONNECTED · PUBLIC-NETWORK RFQ DISABLED" : "APP20 RFQ WORKSPACE"}
<span>MAIL / PAY / FUNDING ARE SEPARATE · PUBLIC BOUNDARIES REMAIN CORRELATABLE</span>
{/* Live session state for the module you are actually in. It used to
name the RFQ workspace on every route, Mailbox included. */}
{connected
? `${moduleName} · WALLET CONNECTED · PUBLIC-NETWORK RFQ DISABLED`
: `${moduleName} · NO WALLET CONNECTED`}
</div>
<header className="app-header">
<Link
Expand All @@ -56,15 +69,11 @@ export default function AppShell({ renderLocalnetTools }: AppShellProps) {
>
Counterparties
</Link>
</nav>
<div className="app-utilities">
<Link
className="request-link"
to="/pay"
aria-current={pathname === "/pay" ? "page" : undefined}
>
<Link to="/pay" aria-current={payActive ? "page" : undefined}>
Pay
</Link>
</nav>
<div className="app-utilities">
<SessionControl />
</div>
</header>
Expand Down
8 changes: 6 additions & 2 deletions src/app/components/session.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -86,9 +86,13 @@
opacity: 0.6;
}

/* Which network is current is state, and the header already has one way of
marking current: the tab's inset accent rule. A solid accent block here also
sat flush against the connect button and read as one control. */
.networkToggle .networkActive {
color: var(--shell-bg);
background: var(--shell-orange);
color: var(--shell-text);
background: var(--shell-inset);
box-shadow: inset 0 -2px var(--shell-orange);
}

.networkToggle span[role="status"] {
Expand Down
7 changes: 6 additions & 1 deletion src/app/contacts/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import { useEffect, useState } from "react";
import { Link } from "@tanstack/react-router";
import SelectWallet from "@/app/components/client/WalletHandle/SelectWallet";
import { useActiveStarknetSession } from "@/app/active-session";
import { storeDeskHandoff } from "@/lib/desk-handoff";
import { MAIL_RECOVERY_PHRASE_AUTHORITY_NOTICE } from "@/lib/mail-authority-copy";
Expand Down Expand Up @@ -243,8 +244,12 @@ function AddressBookPanel({
<div className={styles.bookEmptyBlock}>
<strong>Wallet required</strong>
<p className={styles.bookEmpty}>
Connect a wallet to open this account's encrypted book.
The book is encrypted under the connected account, so it opens
only once a wallet is connected.
</p>
<span className={styles.connectAction}>
<SelectWallet />
</span>
</div>
)}
</div>
Expand Down
53 changes: 53 additions & 0 deletions src/app/css-module-classes.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { execFileSync } from "node:child_process";
import { readFileSync } from "node:fs";
import { dirname, resolve } from "node:path";
import { describe, expect, it } from "vitest";

/**
* A CSS-module class that does not exist resolves to `undefined`: the element
* renders with no styling at all and the word "undefined" in its class list,
* with nothing to notice at build time. Two such references had left a page
* masthead and a form field unstyled before this guard existed.
*/
function componentFiles(): string[] {
return execFileSync("git", ["ls-files", "src/**/*.tsx"], {
encoding: "utf8",
})
.split("\n")
.filter(Boolean);
}

function definedClasses(cssPath: string): Set<string> {
const text = readFileSync(cssPath, "utf8");
const names = new Set<string>();
for (const match of text.matchAll(/\.([A-Za-z_][\w-]*)/g)) names.add(match[1]);
return names;
}

function resolveImport(fromFile: string, specifier: string): string {
if (specifier.startsWith("@/")) return resolve("src", specifier.slice(2));
return resolve(dirname(fromFile), specifier);
}

describe("CSS module references", () => {
it("only uses classes its stylesheet defines", () => {
const undefinedReferences: string[] = [];
for (const file of componentFiles()) {
const source = readFileSync(file, "utf8");
const importMatch = source.match(
/import\s+styles\s+from\s+"([^"]+\.css)"/,
);
if (!importMatch) continue;
const defined = definedClasses(resolveImport(file, importMatch[1]));
const used = new Set(
[...source.matchAll(/styles\.([A-Za-z_][\w]*)/g)].map(
(match) => match[1],
),
);
for (const name of used) {
if (!defined.has(name)) undefinedReferences.push(`${file}: ${name}`);
}
}
expect(undefinedReferences).toEqual([]);
});
});
2 changes: 1 addition & 1 deletion src/app/funding/FundingReadinessPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,7 @@ export function FundingReadinessPanelView({
style={{
display: "grid",
gridTemplateColumns: "repeat(auto-fit, minmax(220px, 1fr))",
gap: 12,
gap: 1,
}}
>
<div>
Expand Down
75 changes: 62 additions & 13 deletions src/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -420,8 +420,7 @@ a {
align-items: center;
gap: 6px;
}
.app-tabs a,
.request-link {
.app-tabs a {
display: inline-flex;
min-height: 44px;
align-items: center;
Expand All @@ -434,24 +433,18 @@ a {
letter-spacing: 0.08em;
text-transform: uppercase;
}
.app-tabs a[aria-current="page"],
.request-link[aria-current="page"] {
.app-tabs a[aria-current="page"] {
border-color: var(--shell-line);
color: var(--shell-text);
box-shadow: inset 0 -2px var(--shell-orange);
}
.app-tabs a:hover,
.request-link:hover {
.app-tabs a:hover {
color: var(--shell-text);
border-color: var(--shell-orange);
}
.app-utilities {
justify-content: flex-end;
}
.request-link {
border-color: var(--shell-orange);
color: var(--shell-text);
}
.network-status,
.wallet-status {
display: inline-flex;
Expand Down Expand Up @@ -483,6 +476,65 @@ a {
overflow-wrap: anywhere;
}

/* The funding gate was the one panel still outside the desk's language: a
plain paragraph for its eyebrow, body-sans labels on its facts, and every
sentence running the full width of the window. */
.funding-readiness-panel > p:first-child {
color: var(--shell-orange);
font-family: var(--font-mono-ui), monospace;
font-size: 10px;
font-weight: 700;
letter-spacing: 0.1em;
text-transform: uppercase;
}

.funding-readiness-panel h2 {
margin: 6px 0 0;
font-size: 20px;
font-weight: 800;
letter-spacing: -0.03em;
}

.funding-readiness-panel h3 {
margin: 18px 0 6px;
font-family: var(--font-mono-ui), monospace;
font-size: 10px;
font-weight: 700;
letter-spacing: 0.09em;
text-transform: uppercase;
color: var(--shell-muted);
}

.funding-readiness-panel p,
.funding-readiness-panel li {
max-width: var(--measure-reading);
line-height: 1.55;
}

.funding-readiness-panel dl {
border: 1px solid var(--shell-line);
background: var(--shell-line);
}

.funding-readiness-panel dl > div {
padding: 9px 11px;
background: var(--shell-bg);
}

.funding-readiness-panel dt {
color: var(--shell-muted);
font-family: var(--font-mono-ui), monospace;
font-size: 9px;
font-weight: 700;
letter-spacing: 0.09em;
text-transform: uppercase;
}

.funding-readiness-panel dd {
margin: 4px 0 0;
font-size: 13px;
}

.panel-heading {
display: grid;
gap: 5px;
Expand Down Expand Up @@ -849,9 +901,6 @@ a {
font-size: clamp(8px, 2.25vw, 9px);
letter-spacing: 0.03em;
}
.app-utilities .request-link {
padding: 0 5px;
}
/* Wrap rather than clip: the Disconnect action stays reachable at 320px. */
.app-utilities {
min-width: 0;
Expand Down
Loading
Loading