feat(ui): UI modernization — sidebar/rows/build, dark-first design system, command center - #212
Conversation
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
39d78cb to
9a1ff9a
Compare
There was a problem hiding this comment.
Pull request overview
This PR modernizes the frontend UI by applying the BitSafe design system across the app (dark-first theme, updated typography/accent tokens), while also improving local development ergonomics with mock-mode API/auth support and richer UI primitives (sidebar collapse, status indicators, approval/workflow visualization).
Changes:
- Reworked MUI theme tokens and global CSS to implement the new dark-first BitSafe design system (colors, typography, table row sizing/insets, component chrome).
- Added build/version metadata injection and surfaced it in the UI; introduced a collapsible sidebar with persisted state.
- Added dev-only mock API server + mock auth flow and expanded JSON fixtures to enable realistic offline UI development.
Reviewed changes
Copilot reviewed 68 out of 70 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| frontend/vite.config.ts | Adds Cargo/version parsing, mock API dev plugin, and build-time defines (__APP_VERSION__, __MOCK__). |
| frontend/src/vite-env.d.ts | Declares build-time injected globals for TypeScript. |
| frontend/src/styles.ts | Updates zebra row styling to match the new dark substrate and accent hover behavior. |
| frontend/src/main.tsx | Loads Roboto Mono font weights for numeric/ID typography. |
| frontend/src/index.css | Introduces new CSS tokens (--font-mono, --accent, --content-pad) and updates light/dark backgrounds. |
| frontend/src/contexts/ThemeContext.tsx | Major design-token overhaul: palettes, typography, component overrides; defaults new users to dark mode. |
| frontend/src/contexts/AuthContext.tsx | Adds dev-only mock auth provider gated by __MOCK__. |
| frontend/src/components/viz/ApprovalViz.tsx | New approval/workflow visualization components (pills, ring, pipeline, card chrome). |
| frontend/src/components/ThemeSwitcher.tsx | Adds orientation support for horizontal/vertical layout. |
| frontend/src/components/StatusDot.tsx | New status dot with optional pulse and reduced-motion handling. |
| frontend/src/components/Sidebar.tsx | Adds collapsed rail mode, build/network footer, updated nav labels/badges. |
| frontend/src/components/PartyList.tsx | Adjusts party table columns/layout and improves wide-screen ID display behavior. |
| frontend/src/components/PartyDetail.tsx | Aligns page sections with shared --content-pad inset token. |
| frontend/src/components/PackagesPanel.tsx | Aligns header padding with --content-pad; tweaks table layout and row padding. |
| frontend/src/components/OnboardingDialog.tsx | Significant UX refresh: peer filtering/selection UI, modal layout, and updated copy. |
| frontend/src/components/NetworkConfigAccordion.tsx | Replaces legacy status icon logic with new StatusDot; updates mono font usage. |
| frontend/src/components/Logo.tsx | Removes build-date/redirect easter egg behavior and simplifies logo text behavior. |
| frontend/src/components/LoginPage.tsx | Redesigns login screen with dark gradient hero and a background motif. |
| frontend/src/components/KickDialog.tsx | Updates copy to match “Approvals” naming. |
| frontend/src/components/HoldingsSection.tsx | Switches numeric typography to Roboto Mono token. |
| frontend/src/components/GovernanceSection.tsx | Switches mono typography usage to Roboto Mono token. |
| frontend/src/components/GovernanceAuditTrail.tsx | Aligns row padding and ensures collapsible detail rows opt out of fixed-height styling. |
| frontend/src/components/CopyableText.tsx | Adds expandOnWide option and switches to Roboto Mono token. |
| frontend/src/components/ContractsDialog.tsx | Switches mono typography usage to Roboto Mono token. |
| frontend/src/App.tsx | Persists sidebar collapsed state; updates layout offsets/transitions; renames “Pending approvals” → “Approvals”. |
| frontend/package.json | Adds dev:mock script and @fontsource/roboto-mono dependency. |
| frontend/package-lock.json | Locks @fontsource/roboto-mono dependency. |
| frontend/mocks/workflows.json | Adds mock workflows fixture for mock-mode UI rendering. |
| frontend/mocks/vaults.json | Adds mock vaults fixture. |
| frontend/mocks/transfer-preapprovals.json | Adds mock transfer-preapprovals fixture. |
| frontend/mocks/transfer-factories.json | Adds mock transfer-factories fixture. |
| frontend/mocks/token-standard-contracts.json | Adds mock token-standard-contracts fixture. |
| frontend/mocks/services_user.json | Adds mock services (user) fixture. |
| frontend/mocks/services_provider.json | Adds mock services (provider) fixture. |
| frontend/mocks/party-config.json | Adds mock party-config fixture. |
| frontend/mocks/participants-status.json | Adds mock participants-status fixture (including workflow status fields). |
| frontend/mocks/packages.json | Adds mock packages fixture. |
| frontend/mocks/packages_vetted.json | Adds mock vetted-packages fixture. |
| frontend/mocks/packages_compare-peers.json | Adds mock peer package-compare fixture. |
| frontend/mocks/operator-info.json | Adds mock operator-info fixture. |
| frontend/mocks/node-config.json | Adds mock node-config fixture (incl. canton network field). |
| frontend/mocks/network-info.json | Adds mock network-info fixture. |
| frontend/mocks/network-config.json | Adds mock network-config fixture (peers list). |
| frontend/mocks/keys_status.json | Adds mock keys status fixture. |
| frontend/mocks/invitations.json | Adds mock invitations fixture. |
| frontend/mocks/instruments.json | Adds mock instruments fixture. |
| frontend/mocks/holdings.json | Adds mock holdings fixture. |
| frontend/mocks/governance_transfer-instructions.json | Adds mock governance transfer-instructions fixture. |
| frontend/mocks/governance_state.json | Adds mock governance state fixture (empty/default). |
| frontend/mocks/governance_state__ibtc-admin.json | Adds party-scoped governance state fixture. |
| frontend/mocks/governance_state__governance-core.json | Adds party-scoped governance state fixture. |
| frontend/mocks/governance_state__cbtc-network.json | Adds party-scoped governance state fixture. |
| frontend/mocks/governance_state__cbtc-mint-authority.json | Adds party-scoped governance state fixture. |
| frontend/mocks/governance_mint-requests.json | Adds mock governance mint-requests fixture. |
| frontend/mocks/governance_known-members.json | Adds mock governance known-members fixture. |
| frontend/mocks/governance_confirmations.json | Adds mock governance confirmations fixture (empty/default). |
| frontend/mocks/governance_confirmations__ibtc-admin.json | Adds party-scoped governance confirmations fixture. |
| frontend/mocks/governance_confirmations__governance-core.json | Adds party-scoped governance confirmations fixture. |
| frontend/mocks/governance_confirmations__cbtc-network.json | Adds party-scoped governance confirmations fixture. |
| frontend/mocks/governance_chain-audit.json | Adds mock governance chain audit fixture. |
| frontend/mocks/governance_burn-requests.json | Adds mock governance burn-requests fixture. |
| frontend/mocks/credential-offers.json | Adds mock credential-offers fixture. |
| frontend/mocks/contracts_query.json | Adds mock contracts query fixture. |
| frontend/mocks/auth-config.json | Adds mock auth-config fixture. |
| frontend/mocks/auth_test.json | Adds mock auth test fixture. |
| frontend/mocks/auth_status.json | Adds mock auth status fixture. |
| frontend/index.html | Updates pre-theme bootstrap script for dark-first default and new dark substrate color. |
| build.rs | Passes crate version into the frontend build via APP_VERSION. |
Files not reviewed (1)
- frontend/package-lock.json: Generated file
Suppressed comments (1)
frontend/src/components/viz/ApprovalViz.tsx:255
var(--font-sans)is not defined in CSS, so this title font will not reliably match the app typography. Consider using the theme’s configured font family.
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
9a1ff9a to
7dd0e12
Compare
Copilot review loop — disposition summary
By category
All three were valid and are fixed, each as its own commit, with the threads replied to and resolved:
Verified after the fixes: Outcome: all round-1 findings addressed; no open items. Note on round 2: re-requesting |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 39 out of 41 changed files in this pull request and generated 1 comment.
Files not reviewed (1)
- crates/decman/frontend/package-lock.json: Generated file
Suppressed comments (1)
crates/decman/frontend/src/components/Sidebar.tsx:145
- In collapsed mode the BitSafe logo is a clickable
<img>with onlyonClick, which is not keyboard-accessible and doesn’t expose button semantics to assistive tech. Wrap it in anIconButton(or add role/tabIndex + key handlers) so the reload action is reachable via keyboard.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 39 out of 41 changed files in this pull request and generated no new comments.
Files not reviewed (1)
- crates/decman/frontend/package-lock.json: Generated file
Suppressed comments (1)
crates/decman/frontend/src/components/OnboardingDialog.tsx:168
toggleAllVisibledecides whether to clear/select based onallVisibleSelectedfrom the render, not theprevset inside the state updater. If the handler fires multiple times before a re-render (e.g. double-click / key repeat), the branch decision can be stale and the toggle won’t actually toggle. Compute the “all selected” check fromprevinside the updater instead.
const toggleAllVisible = () => {
setSelectedPeerIds((prev) => {
const next = new Set(prev);
if (allVisibleSelected) {
visiblePeers.forEach((p) => next.delete(p.participant_id));
Copilot review loop — final disposition summarySupersedes the round-1 table above. Converged: round 3 reviewed the current head and generated no new comments.
By category
Round 2 — the one that mattered
Not theoretical — I built the pre-fix config to check:
(Probed by string literal, not identifier — a production build minifies names, so grepping for
Round 3 — suppressed, taken anyway
Verified on each round: Outcome: converged, no open items, nothing declined. Correction to my earlier note: I reported that re-requesting the bot couldn't trigger another round. That was wrong — the request does work; the bot simply never appears in |
e04c85d to
36948e2
Compare
36948e2 to
ceb90b6
Compare
The footer carried a 4px top margin, which left a strip of page background between the last row and the rule that is supposed to close the list off — most obvious on the packages table, where the rows run right up to it.
Two things held them ~106px off the card's edge: they were centered in slots sized by their legend labels, and every row carried 40px of trailing space to keep the visibility toggle clear of the Create-Party FAB. The columns now right-align — matching how the external-parties list already aligns its own — and the FAB spacer drops out once the column is capped and centered, since the toggle clears the corner on its own by then. That puts the icons 38px from the edge on a wide screen, and the spacer still holds them clear on the narrow widths where the column reaches the corner. Moved the shared slot widths into styles.ts on the way past: exporting them from a component file cost a react-refresh lint error, and they were always layout tokens rather than anything of PartyList's.
Each peer column sized itself to its own header, so a wide grid of identical check/cross cells arrived in a ragged set of widths, and the table only carried a flat 650px floor: adding peers squeezed every column narrower rather than overflowing, since an auto-layout table shrinks to fit before it scrolls. The comparison table now states each column's width under a fixed layout — as the vetted-packages table beside it already did — and its floor grows with the peer count, so peers stay one width, the package name keeps a readable column, and a long peer list overflows into the scroller that was already there. Peer names ellipsize with the full participant id on hover, so a peer without a short name can no longer stretch its column.
The theme pads a table's leading and trailing cells out to `--content-pad` so a full-bleed table lines its edge content up with the rest of the UI. The peer comparison isn't full-bleed — it's a fixed grid inside a scroller — so on a wide screen that rule put 126px of padding inside its last peer cell: the column painted its full width while its tick sat against the left edge and its label clipped to one letter, which read as the columns being unequal. The trailing inset is dropped for this table, and its columns are declared in a colgroup so the widths come from one place rather than being inferred from the cells. The leading inset stays — it lines the package name up with the panel header above it. Measured across every peer column: equal widths, and each tick centred on its own column.
`new Date` yields an Invalid Date rather than throwing, so the try/catch around the build stamp could never fire and a malformed value formatted as `NaN-NaN-NaN NaN:NaN`. It matters because the stamp prefers `buildTime` from the backend, so the input is off the wire rather than compile-time. Checks `getTime()` and falls back to the raw string.
The approval cards asked for `var(--font-sans)`, which was never declared — only `--font-mono` was — so those two labels silently fell back to whatever they inherited instead of the UI face. Declared it next to `--font-mono` and pointed both the root rule and the theme's FONT_SANS at it, so the sans stack is written once rather than repeated as a literal in the stylesheet and the theme.
The prefix rules named the two allowed punctuation characters inside typographic quotes, which reads as though the quotes are part of what you may type. The validator accepts ASCII '-' and '_' only, so the copy now quotes them plainly — in the helper text and the error it mirrors.
`__MOCK__` came straight off `process.env.MOCK` with nothing tying it to the dev server, and it is what swaps the app onto a fake login session. `build.rs` shells out to `npm run build`, so a build environment carrying `MOCK=true` produced a shipped bundle that skipped real authentication — confirmed by building the old config that way and finding the mock session's token string in dist twice, where a build without the variable had none. The flag is now `command === "serve" && process.env.MOCK === "true"`, feeding both the plugin and the define from one place. Rebuilt with `MOCK=true`: the token string is gone from the bundle, and `npm run dev:mock` still serves fixtures.
In the collapsed rail the logo carried the reload action on a bare <img> with an onClick, so it was unreachable by keyboard and announced itself as an image rather than a control. It's an IconButton now, with the mark as its decorative child and the action named on the button.
`toggleAllVisible` branched on `allVisibleSelected`, computed during render, from inside the state updater. Two invocations before a re-render — key repeat on the button — both read the same stale flag, so the second repeated the first instead of toggling back. The updater now derives "all selected" from the `prev` set it was handed. The render-time value stays where it belongs: the button's label and disabled state.
Every card row now lifts on hover, not only the clickable ones. The lists are read across, and the tint is what tracks the eye along a row; the external parties list had no hover at all, because it passes no `onActivate`. A row also takes an optional `detail`, revealed below it behind a chevron in a slot the legend above the rows shares, so the two cannot drift apart. A row is an expander or a control, never both — one click cannot mean two things.
The list said how many participants host a party but not which ones, and the count alone doesn't answer the question an operator has — who is holding this party up. The `PartyToParticipant` mapping the list already reads carries the participants, so `ExternalPartyInfo` now carries them too, each with the permission the mapping grants it. Expanding a row reveals them. The permission chip is uncoloured: Confirmation is the norm for a hosting mapping, so tinting it would paint nearly every row and leave nothing for the exceptions to stand out against. Mapping the proto discriminant to the wire `Permission` stays in the handler, where the other topology reads do it, so the workflow layer keeps its proto types to itself.
The theme insets a table's leading and trailing cell to `--content-pad` but leaves the row rules and zebra fills full-bleed, so on a wide screen every rule ran several hundred pixels past the column it belonged to. The inset now belongs to a wrapper around the table and the cells give it back, which puts the rule's ends where the content's are. Applied to all four tables in the view, and the audit trail's mode toggle and the holdings controls move onto the same inset — they sat against the sidebar while their rows started a column in. Section titles take the weight and the full text contrast that the generic `subtitle2` eyebrow doesn't, and their band keeps its fill whether open or shut: expanded, the band is what separates one section's table from the next section's title. Its fill becomes a named constant in the style module, where the palette carried an unread `surface2` before — MUI has no palette slot for that step, and a constant beside a component would break fast refresh.
`--content-pad` grows with the window, so past 1560px the package name drifted right with it — half a screen of empty gutter before the first column, with the row rules running under it. A package list is read down its first column, so it now sits at the left of the pane at any width with a fixed gutter, the way a Finder list view does. The search bar above moves onto the same gutter. Replaces the comparison table's hand-trimmed trailing pad, which was solving the same problem from the other end.
Confirm and Accept were accent-filled, which is the same red the destructive buttons use, so approving and undoing read alike. Both are affirmative, so both go green, and Revoke joins Cancel in error red — taking a confirmation back and cancelling a proposal are the same kind of act. Execute drops to outlined so the pair reads as one primary step and one follow-on rather than two equal calls to action. The title's value was accent too, which on a proposal whose value is a contract id painted a whole line red. The mono face already sets it apart from the action name, so the colour was doing nothing the type wasn't.
Quorum met, and the card still showed Confirm beside Execute plus a "Ready to execute" pill. The pill said what the Execute button already says, and Confirm invited a vote the action no longer needs. Both go, on the governance and the domain cards alike, and Execute takes the filled treatment back now that it no longer shares a row with Confirm. Revoke stays where you have already confirmed: withdrawing your own vote is still open to you, and it is the only way to take one back before someone executes. Orphaned keeps its pill — that is a problem, not a step.
"Your vote needed" sat beside a Confirm button, "Respond" beside Decline and Accept, and "Running" above the pipeline, the peer count and the current step. In each case the pill named the state the rest of the card was already showing, so a row carried the same fact twice. What is left is the states nothing else accounts for: "Awaiting others", where you have confirmed and the card offers no way forward; "Queued", which has no pipeline yet; "Orphaned"; and the terminal pills on the collapsed rows in Completed, where the pill is the only signal there is.
The Completed footer sat inside the capped card column, so its top rule stopped where the cards did instead of closing off the list across the view. And the column's bottom padding sat under a bar pinned with `bottom: 0`, so the bar came to rest 24px short of the bottom with an empty strip below it. Both are what the parties lists already solved: the bar renders outside the column, and the region is a full-height flex column so the list takes the leftover and the bar lands flush. Its visibility moves out of the section loop with it, so the other filters do not grow a stray bar.
88px sat above the bar: 32 from the shared Container, which for this tab holds nothing but portal-rendered dialogs and so was an empty strip above the content (tabs 0 and 2 already opt out of it), 32 from the tab's own top padding, and 24 from the column's. The bar is sticky at `top: 0`, so all of it was space the first scroll threw away — and the bar jumped as it went. Gone: the bar now rests where it sticks. Below it, the rule was the last thing the bar painted, so a card scrolling under it arrived flush against the rule. The `mb` that looked like the gap belonged to the content and scrolled away with it. The sticky element is now an outer box whose fill runs 12px past the rule, and the card disappears behind that strip instead of touching.
Seating the filter bar at the top dropped the column's `py: 3`, and the bottom half of it went with the top half — so the last card ended flush against the bottom of the page. The padding comes back on the bottom only. The Completed footer bar is a sibling of the column, not a child, so this is the gutter above it and the bar itself still sits flush.
The column read only `build_version` — the display string CI stamps from the image tag. A peer that predates that field reports just its compatibility `version`, and the column drew "—" for it even though its semver was right there in the same response. It now falls back to `version` before giving up, on the self row and the peer rows alike. The mock fixture carried only `version` too, so the whole column read "—" in mock mode and the preview said nothing about what the view does. Its reachable peers now report a build version, and "—" is left to the peers that genuinely report none — the unreachable and handshake-failed ones.
fc47290 to
c319ed3
Compare
Declining an invitation turns the coordinator's workflow down — the same kind of act as Cancel and Revoke, so it takes the same red and the same outline instead of reading as a muted afterthought beside Accept.
The approvals fixtures only ever reached Confirm and Execute. No confirmation was ever attributed to the member party, so Revoke could not appear; no proposal carried a `proposer`, so Cancel proposal could not either; and the orphaned card had an empty confirmations list, so neither Revoke nor Dismiss showed on the one card that exists to demonstrate them. Three of the four destructive buttons and the "Awaiting others" pill were unreachable in mock mode. Adds an action confirmed by the member and short of quorum (Awaiting others + Revoke), one confirmed by the member and over it (Revoke + Execute), a proposal the member proposed (Confirm + Cancel proposal), one both proposed and confirmed (Revoke + Cancel proposal + Execute), and confirmations on the orphan (Revoke + Dismiss). Also reconciles `can_execute` with the confirmation counts. Six cards claimed "quorum met" — the ring follows the count — while offering Confirm instead of Execute, because the buttons follow `can_execute` and the fixtures disagreed with themselves. Four of those predate this branch.
#393 removed the legacy Vault project and renamed the fixture strings it touched, but the rows this branch had added kept the old flavour — so the mock preview still advertised a product that no longer exists. Renamed on #393's own precedent: `bitsafe-vault-*` to `cbtc-governance-*`, and the two remaining `vault-` party prefixes to `treasury-`. No "vault" text is left in any view.
Five edge-to-edge grey bands stacked down the page, each opening onto a dense mono table that ran the full width, with the party's figures reduced to pills wedged between three accent-outlined buttons. Nothing in it had a rank, so there was nowhere for the eye to land. The sections become cards. A shut one is a single quiet strip; an open one gets a filled header its table sits under, and the card's own edge bounds the table, which is a better boundary than the wrapper inset that used to fake one — so `insetTableSx` gives way to `cardTableSx` and the holdings and audit sections move onto the card's gutter with it. The figures become tiles worth reading, at 22px with the label tracked out above them, and Threshold says "2 of 3" now that it has the room. Only the governance parameters go up there: auth and the participant count both appear a few pixels below as their own section's badge, so a tile for either says it twice. The identity block leads with the party's own name, drops the full id and the owner key beneath it at a quieter weight, and gathers the actions on the right in a neutral outline rather than three accent buttons competing with the data.
Main advanced twice while the first merge ran its e2e suite. It landed the UI modernization (#212) and the 1.7.0 version bump (#404). One file conflicted: crates/decman/src/server/types.rs. Main added ExternalPartyHost to the common::api re-export list. This branch had reflowed that same list, because the PR moved the Instrument* and PartyCredentialRequirement types into decman-lib. The resolution keeps this branch's shorter list and adds ExternalPartyHost. The UI PR is almost all frontend TypeScript, which this branch does not touch, so it merged clean. Its Rust delta is additive: an ExternalPartyHost DTO on the external-parties read. The frontend needs a fresh install. The UI PR adds @fontsource/roboto-mono, so a stale node_modules fails the build. `npm ci` from the merged lock file fixes it and leaves the lock file untouched. cargo fmt, cargo clippy --all-targets --all-features -D warnings and cargo test all pass. The frontend builds, tsc -b type-checks the new components against the regenerated wire types, and its six tests pass. The localnet e2e suite passes end to end again: governance_workflows_e2e, 1 passed, 0 failed, 1391s. Its external_party_tenant phase covers the new ExternalPartyHost field on the external-parties read. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Stacked on #211 (base
fix/ui/bugs) — review/merge after it. Developed against mock mode.Summary
Applies the bitsafe-design system across the whole app — dark by default, faithful to the skill's tokens. Layouts are unchanged; this is a design-system pass, not a restructure.
Theme (the bulk)
#0F0E0D/#1E1A17/#2A2420(not flat gray).#D63A0Faction accent — the AA-safe tone, not the decorative#FF6633.Component polish
prefers-reduced-motionhonored.Notes
@fontsource/roboto-mono. Keeps the unified 48px rows + large-screen table inset.