feat(ui): UI modernization — sidebar/rows/build, dark-first design system, command center - #212
feat(ui): UI modernization — sidebar/rows/build, dark-first design system, command center#212schronck wants to merge 47 commits into
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 |
Sidebar collapses to a 56px icon rail (state persisted). Build version + date always shown at the sidebar bottom — version injected from the crate version (build.rs APP_VERSION) with a Cargo.toml fallback for dev/mock — and the build-date/swagger click easter egg removed.
Every list row is a uniform 48px (theme MuiTableCell height) regardless of whether it carries a copy button/chip or just text, fixing the shorter Config-peers and DAR-compare rows. Normalized outlier per-cell paddings; collapsible audit-detail rows opt out via height:auto.
…eline, compact rows
The grouped/filtered approvals feed replaced the flat paginated list that main had meanwhile gained, which left `PaginationControls` referencing state the new body no longer defined, and dropped the `data-*` attributes main put on the invitation and workflow-run cards. Pagination now applies to the Completed section only: "Needs your action" and "In progress" are bounded by how much work is live, while Completed grows without limit. The hook runs above the loading/empty early-returns so its order is stable across renders. ApprovalCard takes a `dataAttrs` bag so the redesigned cards keep carrying the selectors, and the party ids main added use the mono token.
…entity The always-on stamp in the sidebar footer and the build-info reveal main added to the logo were two displays of the same thing off two different sources. The stamp was compile-time only (crate version + bundle date via vite `define`), which names the bundle rather than the image that is actually running. It now prefers the identity the backend reports (CI image tag / short SHA and the time CI stamped it) and falls back to the injected values, which is all that exists before login, under `npm run dev` and in mock mode. The logo's reveal is left alone: on small screens there is no sidebar, so it stays the only way to read the build there.
Surfaces that landed on main after this branch forked still carried the old `ui-monospace, SFMono-Regular, Menlo` stack instead of the design system's `--font-mono` (Roboto Mono), so ids, amounts and page numbers rendered in a different face than the rows around them. The external-parties table was modelled on the pre-redesign parties list and kept its `py: 1.5` cells, which made its rows shorter than every other list; normalized to the uniform 48px row, with the host/confirmation counts and the created timestamp on the mono token.
The hide/unhide column sat under a blank header, leaving the eye icon unexplained. The rigid 56px width would have squeezed the tracked uppercase header, so the cell pair now uses the same shrink-to-fit idiom as Party ID and Auth.
Both parties lists were tables, which spent a row's width on chrome and read
as a grid of cells rather than one party per line. They are now card rows in
the same shape as the approvals feed's Completed section.
The party id splits at `::`: the readable prefix carries the weight and the
namespace fingerprint recedes behind it, ellipsizing to fit. Cutting the tail
is safe here — every namespace opens with the same multicodec bytes and
diverges after — so the row adapts to the viewport without breakpoints.
Extracted the shell the Completed section had inline into RowCard and used it
in all three, so the shapes cannot drift apart. Since a card list has no
header row, the auth and visibility columns are labelled by a legend sharing
the cards' slot widths.
Dropped the external list's separate Fingerprint column: it is documented as
the `{fingerprint}` half of the party id, so the table was showing the same
value twice — the card now shows the whole id, with the full value copyable.
The endpoint was missing from the mock plugin's prefix list and had no fixture, so the External Parties view could not be previewed offline at all.
The bar spanned the full width but pushed the range and the page pills to opposite edges, so the controls sat in the corner rather than under the rows they page through. The bar still fills its container — its rule closes off the rows above — but the range and the controls now travel together, centered on it.
Clearing the Create-Party FAB by padding the container's right side made its horizontal inset lopsided. That is invisible while `--content-pad` is wide, but once it bottoms out at 24px on a small screen the whole list reads as sitting off-center. The clearance moves inside the row instead, as trailing dead space standing in for the spacer cell the table used to carry, so the visibility toggle stays out from under the FAB while the list's own inset stays symmetric.
The Decentralized/External tab row kept a flat 16px inset while the lists below it use `--content-pad`, so on a wide screen the tabs sat ~140px left of the cards they switch between and the column read as off-center. The row arrived with the external-parties view, after the inset was introduced, so it was never brought across. Its labels now line up with the PARTY ID header and the party ids beneath.
The rows belong in the shared inset column, but the footer bar that closes them off should reach both edges — the tables these lists replaced behaved that way, with the footer rule running edge to edge under content that stopped short of it. The footer cancels the inset with a negative margin so its rule and fill reach the edges while its contents stay centered. Moved the inset and the shared legend style into styles.ts on the way past; both lists carried their own copy of the latter.
The skeleton still drew a six-column zebra-striped table — the shape the list had two redesigns ago, before it was trimmed to id + auth and again before it became cards. It flashed a completely different layout than the one that replaced it. Built from the same row shell and column slots as the real list, so the rows don't resize or shift when the data lands.
Both fixtures sat under PAGE_SIZE (18 and 4 against 25), and the footer hides itself when everything fits on one page — so mock mode could never show the paginator, and checking it meant hand-editing the page size. 32 decentralized and 28 external parties: two pages each, the last one partial. Namespaces are generated from a fixed seed, so the fixtures stay stable.
The parties rows carry 30px icon buttons while the external-parties rows are text only, so the two lists sat at visibly different heights side by side. RowCard now sets the uniform 48px row the tables already used, so a row is the same height whatever it happens to carry.
Rows stretched to whatever `--content-pad` left over — 1300px on a wide monitor — which is far past a comfortable measure for a single line of text. They now sit in the same 1040px centered column the approvals feed already used, shared as a token so the two views cannot drift. The footer stays a sibling outside the column, so its rule still runs the full width of the view. Legend, skeleton and the tab row follow the column. Long party ids ellipsize at this measure; the full value stays in the row's tooltip and its copy button.
Only 2 of the 8 pagination instances had enough fixture rows to render their footer, so most of them could not be checked in mock mode at all. Grown past PAGE_SIZE: vetted packages, the peer comparison, holdings, the first party's contracts, and the terminal workflow runs the approvals feed pages. The chain audit now returns `next_before_offset`, which is what makes its cursor control appear.
The id took only as much width as its text, so each row's copy button sat wherever that row's id happened to end — the buttons zigzagged down the list and the gap before the Auth column swung between roughly 50 and 120px. The id now holds the whole column and the button sits at its end, so the buttons form a column and every row breaks at the same place.
The footer is `position: sticky`, which only pins it while something scrolls — on a last page holding a handful of rows it sat right under them with the rest of the view empty below. The parties region is now a column at least a screen tall with the list flexing into the leftover, so the footer lands at the bottom either way. `columnSx` sets its width explicitly for the same reason: inside a flex column the `auto` side margins suppress stretching, which had shrunk the tab row to its content and pulled it off the column's left edge.
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.
e04c85d to
36948e2
Compare
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.