Files
Daniel DimitrovandGitHub 7a3e397346 feat(web): unify workspace & trusted account navigation (WA-2811) (#8271)
* feat(web): add workspace and trusted-safes data hooks

- useWorkspacesView: signed-out/no-workspaces/has-workspaces view state
- useSpaceSafesById: lazy per-space safes for on-demand lists
- useSafeSpaces: reverse lookup from a Safe address to its workspaces
- useLocalAccountsView: connect-wallet/add-trusted/list state machine

* feat(web): unify welcome nav around Workspaces and Trusted accounts

- Tabs: Workspaces first, Trusted accounts second, always visible
- Root landing: added safes -> Trusted accounts, otherwise Workspaces
- Workspaces tab lists spaces as expandable rows that lazy-load safes
- Signed-in users with one space jump straight to it; no forced
  create-workspace redirect for new users

* feat(web): add trusted safes empty-state card

* feat(web): split space safe accounts into Workspace and Trusted tabs

The Trusted tab lists the user's local safes with a per-row
add-to-workspace action and connect-wallet/empty states.

* feat(web): add Workspace/Trusted tabs to the in-safe account dropdown

The selector exposes both lists via a segmented control: the workspace
safes (or a sign-in CTA outside a space) and the trusted safes with
manage/add actions.

* feat(web): redesign trusted accounts as a sortable table

Reusable SafeAccountsTable with Name/Threshold/Networks/Workspaces/
Pending/Balance columns, sortable by name, threshold, networks and
workspaces. Multi-chain accounts expand into per-chain child rows and
sort as one group. Rows show the full address; long names truncate
with a hover tooltip. Column set is configurable for reuse on other
surfaces. Widens the welcome accounts column to fit all columns per
the design.

* feat(web): remove the require_login gate

Users are never forced to sign in: the welcome tabs, trusted accounts
and deep links work signed-out, so the gate, its ?next= round-trip
rules in the route guard, the auth-gate page blanking and the classic
view escape hatch are all removed. The REQUIRE_LOGIN and CLASSIC_VIEW
feature flags become no-ops and are dropped from the FEATURES enum.
Deploying this build launches the signed-out experience; the config
flag no longer has any effect.

* fix(web): give the accounts table a white panel background

The table is a white rounded panel inside the tinted accounts
container, per the design; the container itself keeps its tint.

* fix(web): move Manage trusted Safes onto the Trusted Safes header row

The button sits right-aligned next to the section label instead of
centered under the table, per the design.

* feat(web): align safe selector rows with the accounts table

Dropdown rows (both Workspace and Trusted tabs) show the safe name and
address, a threshold pill, the networks stack, a pending-transactions
badge and the balance — the same presentation for both tabs. The
trigger shows the threshold as an inline pill instead of an avatar
overlay. Threshold/pending pills move to a shared AccountBadges module
used by the accounts table and the selector.

* feat(web): reuse the accounts table on the workspace Safe accounts tabs

Both the Workspace and Trusted tabs render SafeAccountsTable instead of
the card lists. The table gains a renderActions override (workspace
context menu / add-to-workspace button), similarity flagging for the
address-poisoning warning, and a configurable actions column width.
The card-only list components are removed.

* fix(web): show the Safe logo on signed-out welcome pages, align tab widths

The welcome tabs hide the Topbar while signed out, leaving no logo —
render it top-left. The Workspaces tab container widens to match the
Trusted accounts tab so switching tabs doesn't shift the layout.

* feat(web): keep the top bar visible on welcome pages, add account menu

The welcome tabs always show the Topbar so the Safe logo and the
wallet section stay visible regardless of auth state; the logo
replaces the search input as the left content there. A round account
icon with the sign-out popover sits next to the wallet section
whenever the user is signed in to a space account, so signing out no
longer requires entering a space. The signed-out header spacer is
removed since the Topbar now always occupies that space.

* fix(web): align table names in a fixed identicon column, drop the chevron

The identicon sits in a fixed-width slot so every name starts at the
same x position, and multi-chain rows lose the expand arrow — the row
itself stays clickable (aria-expanded), matching the design.

* fix(web): center table badge columns, monospace addresses, slim pending

Threshold, networks, workspaces and pending center-align (name stays
left, balance right), the pending column narrows with a nowrap badge,
and full addresses render in a monospaced font.

* fix(web): style the topbar account button like the other icon buttons

Drops the 48px green-tinted avatar circle in favour of the plain icon
at the same size as the neighbouring topbar buttons.

* feat(web): center welcome tabs, move actions into panels, add workspaces table

* feat(web): open workspaces directly from the welcome list

- Welcome workspace rows no longer expand to list safes; the whole row
  links into the workspace
- Move the Create button above the list as an outline button and drop
  the table header to match the design
- Restyle the invite banner: compact header with Decline/Accept
  design-system buttons, muted workspace row, centered inviter
- Remove the now-unused useSpaceSafesById hook

* feat(web): redesign the safe switcher dropdown and topbar per Figma

Topbar: the safe selector, nested-safes and network controls now render
as muted 40px chips inside one white 56px pill, mirrored by the actions
cluster on the right; chip buttons fill their chips so hover recolors
the whole element.

Trigger: full address with emphasised head/tail (middle ellipsizes
first), CSS name truncation, icon-only threshold badge for multi-chain
safes, foreground/medium balance.

Dropdown: search above the tabs filtering both lists with live counts
in the tab labels, fixed table-like row columns (empty cells keep their
width), full row addresses, hover rename/copy/explorer actions, name and
address tooltips (1s delay, non-interactive), the current safe stays in
the list highlighted, pointer highlight no longer sticks to the last
focused option, and a "Manage trusted accounts" footer row replaces the
two footer buttons.

* feat(web): rework workspace accounts, trusted-safes picker and nav

Rework the workspace Safe accounts experience per the Workspaces UX review:

- SafeAccounts page: drop the Workspace/Trusted tabs, show only workspace
  accounts with a name/address/network search bar; keep multi-chain expansion.
- Add accounts: two-view modal — a trusted-only picker plus an inline
  "Manage trusted Safes" view (owners endpoint + address-poisoning alerts)
  that saves and returns to the picker. Extract ManageTrustedSafesContent.
- SafeAccountsTable: optional selection (checkbox) mode; in selection mode the
  whole row toggles and no longer navigates. Manage view shows a copy button
  and block-explorer link next to each address via AddressWithCopy.
- Sidebar: swap the logo for a back-arrow pill when inside a space or safe,
  linking back to /welcome/accounts.
- SpaceSafeBar: the trusted-accounts tab lists pinned safes only.

* feat(web): manual sort and drag-and-drop across Safe account lists

- Add Manual order option (persisted per scope) alongside Name/Last visited
- Drag-to-reorder trusted safes on welcome and spaces; grip sits in the Name gutter so no column is added
- Apply the shared sort and the sort toggle in the Manage trusted Safes modal
- Always surface trusted accounts; remove the redundant "Safes detected on this browser" sign-in alert

* feat(web): redesign Select Safes onboarding with unified table

- Replace the card list with the shared SafeAccountsTable, split into
  Trusted and Owned sections (owned shown only when a wallet is connected)
- Drop select-all in favour of a per-cap "N of 40 selected" counter + search
- Apply address-poisoning protections to owned safes only: high-similarity
  flag, security banner and a confirm-on-select dialog (trusted safes are
  treated as vetted and never flagged)
- On Next, add each selected owned safe to the global Trusted list
- Fix the onboarding panel width so it stays constant across all steps
- Make the skip links bolder on Select Safes and Invite Members
- Remove the now-unused SafeCard, SafeCardLayout, SelectAllToggle,
  useSelectAll and selectAllHelpers

* fix(web): keep full safe address visible and fix selector tooltips

Safe selector trigger:
- Overlay the copy/explorer/env actions on the address on hover instead
  of reserving inline width, so the full address stays visible and the
  row no longer jumps when the actions appear.
- Reclaim width (8px gaps, drop the balance min-width floor, tighter
  chevron hit-area); the balance column stays protected.

Row/trigger tooltips (copy, explorer, rename):
- Non-hoverable + pointer-events-none, so they close when the pointer
  leaves the button and never block clicking the row beneath.
- Open after a shared 400ms delay, matching the name/address tooltips.

* fix: empty state for trusted safes for connected wallet

* refactor(web): drop dead nav hooks, dedupe safe grouping, surface add-accounts errors

- delete unused useLocalAccountsView and useWorkspacesView (+tests, exports)
- extract triplicated _groupAndSort into @/hooks/safes
- replace swallowed console.log in AddAccounts submit with a user-facing error

* refactor(web): dedupe row icon buttons, share badge, tidy conventions

- extract RowIconAction; Copy/Explorer/Rename buttons become thin wrappers
- migrate onboarding ThresholdBadge to shared AccountBadges
- split SafeAccountTableRow cell rendering into RowCell (under the size limit)
- unify the 'Manage trusted Safes' label; sentence-case + workspace casing fixes
- use success theme tokens for the widget count pill; use safeIds helpers
- rename useSelectAll.types -> addAccounts.types
- add stories (AccountBadges, AddressWithCopy, GetStartedCard, TrustedAccountsActions) + TrustedAccountsActions test

* fix(web): refine safe selector dropdown empty states and multi-chain rows

- Show a "No trusted accounts" empty state with a Manage list button when
  connected without trusted safes, and drop the redundant footer row there
- Update the disconnected empty state copy and close the dropdown before
  opening onboarding so the wallet modal is no longer hidden behind the popup
- Align expanded multi-chain rows (name, threshold, chain icon, pending,
  balance) with the summary row and drop the per-chain identicon
- Auto-expand the active multi-chain group and highlight only the active
  network row instead of the whole summary item
- Support a function-form emptyStateOverride that receives a close callback

* fix(web): unify the trusted-safes CTA label in the dropdown empty state

* feat(web): new empty state for trusted safes

* feat(web): show footer on welcome accounts and spaces pages

The flexbox sticky-footer scaffolding already lives in PageLayout; the
footer was just gated off these routes. Add them to the footerPages
allow-list and cover the visibility logic with a colocated test.

* feat(web): restore brand-green glow on welcome list pages

The glow (previously .authShell) was dropped when the welcome nav was
unified. Reinstate it at the layout level so it sits behind both the
Trusted accounts and Workspaces tabs, and remove the now-dead .authShell.

* fix(web): show Safe name/address tooltips only when truncated

Adds a TruncatedText component that reveals the full value in a tooltip only when the text is actually clipped (measured lazily on hover, not a resize observer), and clip-gates the row address tooltip via the FullAddress middle span. Also adds a closeDelay prop to the shadcn tooltip.

* refactor(web): share one account-row identity cell across dropdown and tables

Moves the dropdown's SafeInfoDisplay and its row primitives (RowIconAction, Copy/Explorer/Rename buttons, FullAddress, TruncatedText) into components/common/AccountRow so both the safe-selector dropdown and SafeAccountsTable render rows from the same component. The table's Name cell now uses SafeInfoDisplay (clip-gated name/address tooltips, copy/explorer revealed on row hover) via leading/hideAddress/badge slots, and the obsolete plainCells/showAddressActions props are removed from the table and its callers.

* refactor(web): drop the workspaces column inside a workspace, align badge sizing

Inside a workspace every Safe belongs to it, so the Safe accounts screen no longer shows the Workspaces column. Workspace avatars are bumped to 24px to match the network logos (column width 116->96px), and each reveals its full workspace name via a hover tooltip instead of a native title.

* feat(web): green highlight for the active safe row, grey on hover

The selected safe in the dropdown now uses a subtle green contained pill instead of grey, matching the design's Active state; hover stays grey. Table rows gain the same grey hover so the identity cell's copy/explorer icons read as hover-revealed.

* fix(web): grey (not green) hover on the account tables

The base MUI theme tints every table row green on hover via a high-specificity '& .MuiTableRow-root:hover' override, which beat the row-level sx. Override it at the Table level with the same grey the safe-selector dropdown uses (--muted); locked rows opt out via data-disabled.

* feat(web): round and inset the account-table row hover to match the dropdown

The row hover now paints a grey pill on the cells (not the tr), inset from the panel edges via 4px transparent side borders + background-clip on the outer cells and rounded outer corners — the same trick the header bar uses. This gives the account tables the contained-pill hover the safe-selector dropdown has, and aligns body content with the header.

* fix(web): remove green corner bleed on row hover, clear the bottom edge

The base theme's green row-hover tint on the <tr> was showing through the inset/rounded grey pill at the corners; suppress the <tr> hover so only the grey cell pill paints. Also add bottom padding so the last row no longer sits on the panel's rounded border.

* fix(web): render the row separators on the account tables

The base theme sets cell borderBottom:none with a specificity the per-cell showDivider sx couldn't beat, so no separator showed. Draw it at the Table level keyed off a data-divider attribute (border.light, absent on the last row of a group/list), matching the design.

* fix(web): float the row hover pill clear of the separators

Give the row cells transparent top/bottom borders (with background-clip) so the grey hover pill insets vertically and no longer touches the separators, matching the design. The separator moves from the cell border to a 1px gradient line on the <tr> (inset 4px), freeing the cell borders for the pill inset.

* fix(web): float the bottom of the hover pill too

The pill's bottom still touched the separator because the base theme forces cell borderBottom to none at a specificity the per-cell sx can't beat, so the transparent bottom inset border was dropped (only the top, which has no theme rule, worked). Move the vertical inset borders to the Table level where they win.

* feat(web): add the hover rename pencil to the account tables

* feat(web): white content card for welcome accounts & workspaces

Lift both welcome tabs onto a shared white rounded card so their content reads against the page's gradient backdrop. A new WelcomeContentCard wraps the Accounts search + table and the Workspaces Create + list (whose box also gains a border). Non-admin workspace rows now show a disabled kebab with a "You need admin access to edit." tooltip, and the accounts search input gets a visible border, the placeholder "by name, address or network", and an updated accessible label.

* fix(web): show data widget in empty state, tighten card spacing

Render the export/import DataWidget when a wallet is connected but has no
trusted Safes, and drop the empty-state cards' bottom padding so the widget
sits 24px below the main content.

* feat(web): restyle sidebar workspace switcher and back arrow

- Give the workspace switcher a persistent grey active background (per Figma),
  replacing the green sidebar-accent hover
- Align the top-bar back-arrow pill with the workspace avatar (both at 20px)
- Inside a Safe, render the back-to-workspace button with a trailing back
  chevron instead of a leading one, matching the space-selector layout

* feat(web): restyle welcome account tabs and import widget

- Match the Workspaces / Trusted accounts tabs to Figma: full-width
  segmented control, white active pill with concentric radius and soft shadow
- Rebuild the "Import your Safe data" widget with shadcn components
- Reserve the scrollbar gutter so content doesn't shift horizontally when a
  page toggles between scrollable and non-scrollable states

* fix(web): reposition top-bar popovers/toasts and consolidate sign-out

The unified top bar is taller than the legacy --header-height, so its
dropdowns and toasts rendered on top of it.

- Account, WalletConnect, wallet and notifications popovers open with a
  12px gap below their trigger instead of pinning to --header-height
- Toast stack cleared below the bar (--header-height + --space-4)
- Account menu popover gets a white background; WalletConnect and the
  account popover adopt the notifications popover's 24px radius
- Remove the duplicate workspace sign-out from the sidebar footer; it now
  lives only in the top-bar account menu (getSidebarProfileInfo relocated
  to HeaderNavigation)

* feat(web): reuse shared accounts table in workspace dashboard widget

Rework the workspace dashboard Accounts widget to reuse the shared
SafeAccountsTable (the component behind Trusted accounts, welcome
accounts, and the space accounts page) instead of bespoke row
components, unifying the design.

- Add an opt-in 'embedded' variant to SafeAccountsTable: no column
  header, no bordered container, and a colgroup so the Name column
  flexes while stat columns stay fixed (fits the card, no scroll)
- Widen onLinkClick to carry the clicked AccountLine (backward
  compatible) so the widget keeps per-safe click analytics
- Delete AccountWidgetItem, ExpandableAccountItem, AccountItemContent
  and drive the widget from items: AllSafeItems
- Show up to 5 accounts, widen the widget (grid 7/5), add a +N overflow
  badge (total - displayed), tighten the View all button padding
- Remove the Pending widget's View all action
- Update legacy Cypress selectors to the shared table's testids

* feat(web): hide add accounts button for space members

* fix(web): let multi-chain safes expand and navigate while manually sorting

* feat(web): float the manual-sort drag handle in the left gutter on hover

* refactor(web): move shared safe helpers out of feature barrels to break import cycles

isMultiChainSafeItem, FiatBalance and useSafeSpaces were reachable only through the
multichain/spaces feature barrels, forming mutual import cycles (myAccounts <-> spaces,
multichain <-> hooks/safes). They depend on shared primitives only, so relocate them:

- isMultiChainSafeItem -> hooks/safes/isMultiChainSafeItem.ts (dependency-free leaf)
- FiatBalance -> components/common/FiatBalance (generic currency formatter)
- useSafeSpaces/SafeSpacesMap -> hooks/useSafeSpaces (store + gateway API only)

No behaviour change; same symbols, same call sites. Reduces the latent cycles that
crash Storybook's webpack module init.

* feat(web): align sidebar accent, new-tx and add-to-workspace buttons with Figma

- Fix dark-mode --sidebar-accent (#171717 → #052e16) so active menu
  items and the back-to-home button show the green highlight; remove the
  grey .sidebarInteractive overrides that compensated for the broken token
- New transaction button: filled secondary → outline (bordered) variant
- Add Safe to workspace: 16px radius, --border dashed, rounded-square icon
  chip, centered content, 12px/semibold label

* fix(web): reclaim content space when sidebar collapses on Safe routes

The collapsed-sidebar padding was gated on isSpaceRoute, so on Safe routes
collapsing the sidebar to icon mode left the main content offset by the full
230px sidebar width — a large empty gap. Generalize the state + .main padding
class to every sidebar route (guarded by isSidebarVisible) so the content
reclaims the freed space on both Safe and space routes.

* feat(web): align trusted-safes modals with Figma copy and button layout

Update the "Manage trusted Safes" and "Trusted Safe accounts" modals to match
the latest Figma:

- SecurityBanner: reword the impersonation warning and shorten the link to
  "Learn more".
- ManageTrustedSafesContent: lay out the footer as two equal-width buttons
  (secondary + Save), with a back chevron on the "Back" label.
- AddAccounts: reword the info banner to "What are trusted Safe accounts?",
  rename the action to "Manage list", and place "Add manually" and
  "Add accounts" side by side.
- AddManually: render the trigger with the shadcn secondary button so it
  matches the primary button's height and fill.

* fix(web): unify header popover radii and sizing

Give the connected-wallet popover, safe/network selector dropdowns, and the
Nested Safes popover the 24px radius used by the WalletConnect popover, so all
header surfaces read as one system.

Also drop the chain-selector dropdown 12px below the taller header pill (was
touching its bottom edge) and let the Nested Safes popover size to its content,
capped at the viewport, instead of always rendering full-height.

* fix(web): remove scrollbar-gutter reservation

The stable scrollbar gutter double-reserved space against the modal/popover
scroll-lock padding, shifting the page when dialogs opened, and left a
mismatched strip in dark mode. Revert to the previous behaviour.

* fix(web): move block-explorer links onto per-chain rows

A multi-chain safe's summary row has no single chain — its chainId is
just the first network's — so the explorer link it carried pointed at an
arbitrary chain. Drop it from the summary/parent rows and render a
per-chain explorer link on each network row instead:

- SafeInfoDisplay: when the address is hidden, place the explorer link
  next to the name; add a nameVariant prop for the bolder table name.
- SafeAccountsTable: child rows keep the per-chain link (address hidden),
  the multi-chain parent gets none; drop the now-unused child chain icon
  and the indent field, aligning child names via a blank icon spacer.
- SafeSelectorDropdown: extract NetworkRow to own the per-chain link;
  the summary row no longer receives one.
- Hoist the shared HOVER_ACTION_CLASS into AccountRow/utils for reuse.

* feat(web): rename Trusted accounts to My accounts

- Rename "Trusted accounts" / "Trusted Safes" UI copy to "My accounts"
- "Manage trusted Safes" buttons -> "Manage list"; modal title -> "Manage my account list"
- Rewrite the connected empty state with a "Learn more" link to the address-poisoning article
- Rename the workspace sidebar "Home" -> "Dashboard"
- AddAccountsChooser: first row -> "Select from my accounts", drop the row subtitles
- Update pin tooltips/toasts, NonPinnedWarning, MigrationPrompt and the Safe Shield warning to match
- Update tests, stories and Cypress page objects accordingly

Analytics event strings and code identifiers/testids are intentionally left unchanged.

* feat(web): show Safe logo and Home label in place of sidebar back arrow

* feat: add drag and drop to the safe list dropdown

* fix(web): show generic Workspace tab label when safe is in no space

The Safe selector dropdown reused the fallback space name (resolved by
useCurrentSpaceId from last-used/first space) in the out-of-space branch
of the workspace tab label. This leaked a specific space name onto the tab
even when the current safe belonged to no workspace. Use the generic
"Workspace" label unless the safe is actually part of the space.

* fix(web): align space Safe account context menu with Figma

Use neutral lucide Pencil/LogOut icons and rename the remove action to
"Remove from workspace" to match the updated design.

* fix(web): show pointer cursor on workspace selector rows

DropdownMenuItem defaults to cursor-default; add cursor-pointer to the
workspace selector's shared item class so hovering actionable rows shows
a pointer.

* feat(web): enlarge safe selector dropdown rows and max height

Bump dropdown rows to ~60px to match Figma and raise the list ceiling from 34rem to 44rem. The height stays gated by base-ui's --available-height, so it still shrinks and scrolls on short viewports.

* fix(web): expand multi-chain safes in the manual-sort dropdown

In Manual sort a multi-chain safe rendered as a flat row that navigated straight to its first chain, so it could not be expanded. Reuse MultiChainSafeItemRow via a new leading slot: the summary now expands/collapses, the grip drags the whole group, and the per-chain rows navigate.

* feat(web): add tooltips to safe selector row stats

Delayed hover tooltips on the threshold, network logos, and pending badge so the row's numbers are self-explanatory. Matches the app's other row tooltips (TOOLTIP_DELAY_MS).

* feat(web): add drag-and-drop reordering to the manage accounts list

Reuse SafeAccountsTable's reorder path in the Manage my account list modal
when Manual sort is active (suppressed while searching), persisting the shared
trusted-accounts order.

- Forward selection state through ReorderableBody so rows show both a checkbox
  and a drag grip; the grip hosts on the leading cell.
- Portal the lifted row to <body> while dragging so the dialog's centering
  transform can't become its containing block and offset it.
- Render the grip inline in the checkbox cell's padding on hover, so switching
  sort modes no longer shifts the table sideways.
- Give the search input a visible border (--input is white) matching the sort
  toggle.

* fix: don’t display drag and drop hint

* feat(web): move connect-wallet hint to manage account list

The workspace picker now lists only trusted Safes, so the 'connect a
wallet to discover accounts' hint no longer fits there. Move it into the
Manage account list view, where connecting discovers owned Safes.

Raise the web3-onboard connect modal z-index above shadcn dialogs so it
stacks on top of the manage list instead of behind it, keeping the list
open and the user's position intact.

* fix(web): keep safe selector open when renaming from the dropdown

Renaming a safe from the top-bar selector used to close the dropdown
because the rename dialog would otherwise render behind the popup and
base-ui closes the popup on outside interaction.

Layer the rename dialog above the popup (z-index) and add a keepOpen prop
that suppresses the popup's close-on-outside-interaction while the dialog
is open, so the user keeps their place in the list.

* fix(web): make onboarding search border visible and refine scrollbar

The Select Safe accounts search relied on the default border-input token,
which is white in light mode and invisible against the card. Use
border-border to match the sibling account search.

Apply the repo's thin themed scrollbar convention to the shared
onboarding scroll container and add right padding so the scrollbar no
longer sits flush against the table.

* fix: align view all

* refactor(web): dedupe safe selection/reorder helpers, drop dead code

- Extract shared getSelectedLeafKeys/applySafeSelectionToggle so the
  onboarding picker and workspace "Add accounts" share one toggle
- Extract reorderByKey; remove the duplicate reorderAddresses helpers
- Slim useSpaceAccountsData to loading/error/refetch (its accounts
  output was discarded) and delete the now-unused AccountsWidget types
- Remove dead code: MyAccountsV2 AccountItem rows, TrustedSafesModal
  SimilarityWarning + styles.module.css
- Focus dropdown rows on hover to clear a stale base-ui highlight

* fix: remove drag handle helper text

* fix(web): render Add manually dialog above the accounts modal

* feat(web): make the whole Safe account row clickable

Extend SafeAccountTableRow so a click anywhere on a row navigates to the
safe (leaf rows) or expands it (multi-chain group rows), instead of only
the name cell. The name keeps its real <a> for keyboard focus and
modifier-click, and copy/explorer/rename/actions keep their own behaviour
via a closest() guard. Selection surfaces are unaffected.

Applies to all navigable account tables (Spaces accounts, Welcome/My
accounts, dashboard widget).

* feat(web): allow renaming accounts from the Manage list modal

* fix(web): raise private-key connect dialog above the onboard modal

The onboard connect modal was bumped to z-index 1450 (above shadcn dialogs),
which pushed it above the private-key popup it spawns (z-index 1400), covering
the input. Raise the popup to 1451 so it stays on top. Fixes wallet-connect
Cypress smoke failures.

* chore: remove alert from manage trusted safes

* refactor(web): remove standalone welcome screen for tabbed welcome

The legacy /welcome "Get started" login screen is replaced by the tabbed
welcome pages (Workspaces + Trusted accounts). /welcome becomes a redirect
stub, and the login gate, home links, and WalletConnect handoff now resolve
to the default tab based on locally trusted safes (getWelcomeRoute), mirroring
the root / logic. WalletLogin and useSignInRedirect are kept — they are reused
by the spaces sign-in flow.

* tests(web): drop obsolete space-notice assertions in TrustedSafesModal

The workspace-notice alert was removed in 8b67837c2 but the tests still
asserted it. Remove the now-stale/vacuous space-notice cases.

* fix(web): mark counterfactual safes as not activated in the accounts table

The unified SafeAccountsTable balance cell only rendered a fiat balance or
skeleton, so undeployed (counterfactual) safes on /welcome/accounts showed a
blank balance instead of the "Not activated" badge every other unified surface
shows. Carry undeployed/isActivating on the row model and render NotActivatedBadge
in the balance cell, matching the dropdown SafeItem and spaces SafeCardReadOnly.

* chore(web): drop dead imports in ManageTrustedSafesContent

Remove the Info icon, Alert/AlertDescription and useIsQualifiedSafe/isInSpace
left unused after the manage-trusted-safes alert was removed; they were failing
the lint step.

* feat(web): add "Add accounts" chooser to My accounts tab

Add an "Add accounts" chooser (Select existing / Create new) to the
personal My accounts list toolbar and the connected-but-empty state,
giving users outside a Space a way to watch or create Safes.

Extract the shared ChooserRow into components/common and reuse it in
both the Space and personal choosers. Update the empty-state card copy,
layout, and "Learn more" link to match the Figma design.

* fix(web): make read-only safe indicator an icon with tooltip

The full "Read-only" text widened the per-network dropdown row and pushed
the explorer link and stat columns to the right, breaking the menu layout.
Replace it with an icon-only indicator plus a "Read-only safe" hover tooltip.

* feat(web): flag pending txs awaiting the connected wallet's signature

The safe-selector dropdown and My accounts table surfaced only the total queued
count, and single-chain rows dropped even that. Thread the overview's
awaitingConfirmation through both, and render an orange dot on the pending badge
with a tooltip breaking down how many pending txs need the wallet's signature.

* tests(web): align safe_selector e2e with the unified navigation UI

Repoint the accounts_modal/safe_navigation page objects at the selector dropdown,
Manage-list modal and rename dialog; drive add-safe through the AddAccountsChooser;
assert the awaiting-confirmation dot; drop the obsolete pin-toggle test (adding to
the trusted list is covered by the dashboard action-required test).

* tests: fix multichain group selectors in safe-selector POM

The unified-nav dropdown renders a multi-chain group as a Collapsible: the
name + rename pencil live in the collapsible trigger (SafeInfoDisplay), not a
select-item, and an active group expands by default so each network sub-row
repeats the summary's chain logos.

- renameSafe / clickSafeOptionsBtn: target select-item OR collapsible-trigger
  so both single safes and groups resolve the rename pencil.
- verifyMultichainSafeChainLogos: scope the logo count to the collapsible
  trigger so expanded sub-rows don't double it.

multichain_safe_selector 5/5, smoke/safe_selector 1/1, regression/safe_selector
30/30.

* refactor(web): remove orphaned AccountsModal

Superseded by the safe-selector dropdown (Workspace/My accounts tabs) and the
Manage list modal; nothing rendered it anymore.

* chore(web-tanstack): drop require shim orphaned by AccountsModal removal

The scoped require() polyfill and compat/require-shim.ts existed solely to
shim require('blo') in SpaceSafeBar/AccountsModal/shared.tsx, which was
removed with the unified-nav rework. No other bundled apps/web source uses a
sync require(), so the vite plugin, the compat module, and its main.tsx import
are all dead. web-tanstack type-check passes.

* tests: adapt spaces basic flow to nav rework; make it idempotent

- sign in via the reworked workspace card (signInWithWallet) and target
  the SpaceRow (space-row) instead of the removed SpaceCard
- navigate onboarding skip + space sections client-side instead of full
  cy.visit reloads, which rebooted the app and raced the render (the
  invite-members skip button rendered after the command timed out)
- add-manually into a fresh, empty workspace so an already-added Safe
  can't leave the confirm button disabled; self-delete afterwards
- clean up the invite test by re-signing as the admin (the invitee is a
  member and cannot delete the workspace)
- drop dead helpers (getSpaceId, openFirstSpaceFromSpacesView)

* fix(e2e): view all acount

* fix(web): resolve correctness issues from PR review

- useSafeSpaces: key workspace membership by chainId:address so a Safe
  sharing an address across chains no longer inherits another chain's
  workspace; reset isResolving on the empty-spaces path so isLoading
  can't stick true (Codex, Clovis)
- Add accounts picker: don't cascade a group toggle onto locked
  workspace children, and wait for the space-safes query before seeding
  the reset form so Save can't diff existing members as removals
  (Codex, Clovis)
- Multi-chain dropdown rows: render each network row's own
  threshold/owners from its per-chain overview instead of the parent
  aggregate (Codex)

* refactor(web): make row copy/explorer buttons tracking-agnostic; drop dead AddressWithCopy

- CopyAddressButton/ExplorerLinkButton hard-coded a SIDEBAR_ELEMENT
  analytics label but are now rendered in the table and dropdown, so
  every copy/open emitted a bogus 'sidebar' event. Remove the baked-in
  tracking and expose optional onCopy/onOpen callbacks so each call site
  labels its own event (Clovis)
- Remove AddressWithCopy: no production consumers (only its own
  story/test), superseded by SafeInfoDisplay + FullAddress (Clovis)

* tests: adapt Safe creation e2e to redesigned welcome flow

Two UI changes on this branch broke every create-safe spec:

- Entry point: /welcome/accounts now renders MyAccountsV2, where creation
  lives behind the 'Add accounts' chooser instead of a standalone
  create-safe-btn. Update the shared clickOnCreateNewSafeBtn to open the
  chooser and pick 'Create new' — this fixes the entry for all 8 flows
  that go through startCreateSafeFlow/connectWalletAndCreateSafe.

- Pay-later now requires a workspace SIWE sign-in before a counterfactual
  Safe can be created; without it the review step falls back to deploy.
  Replace the stale selectPayNowOption() in create_safe_cf with
  clickOnSignInToWorkspaceBtn(), matching the multichain create specs.

* chore(web): drop unused OVERVIEW_LABELS enum members

welcome_page and owned_safes_modal lost their last consumers when the
nav rework removed the welcome page / owned-safes modal UI. knip flags
unused enum members as errors (default severity), which was the sole
cause of the failing knip:exports CI job.

* tests: align spaces dashboard e2e with shared account components

The branch replaced bespoke spaces UI with shared components, changing two
testids the dashboard regression relied on:

- Safe accounts page now renders SafeAccountsTable; count top-level
  account-table-row rows instead of the removed safe-list-item cards.
- Safe header threshold is the shared ThresholdBadge (account-threshold),
  not the removed safe-selector-threshold.

* fix: multichain regression tests

* fix: close Update workspace modal after saving a new name

Renaming a workspace from the space list's three-dot menu left the
modal open on success. The onClose callback was passed to
UpdateSpaceDialog but never threaded to the form or the save handler,
so it only fired on manual dismiss.

Thread onClose through UpdateSpaceDialog -> UpdateSpaceForm ->
useUpdateSpace and invoke it after the mutation succeeds (matching the
EditContactDialog/EditMemberDialog pattern). Errors keep the modal
open with the message shown.

* fix(web): improve account list sorting for name, networks, threshold

The account-list dialogs (Add to workspace, Manage my account list, and
every shared SafeAccountsTable surface) sorted poorly:

- Name: unnamed safes kept raw input order instead of falling back to
  address; the column toggle also never reversed an all-unnamed list.
- Networks: sorted by the number of chains, so single-chain lists never
  reordered. Now sorts by chain identity (Ethereum before Polygon).
- Threshold: sorted on M only, so 2/3 and 2/15 tied. Now breaks ties by
  owner count N (2/3 before 2/15).

Fixes the shared comparators (nameComparator + compareGroups) so all Safe
lists stay consistent.

* fix(web): gate account-table column sorting to Name sort mode

The account lists carried two competing sort controls: the Name/Last
visited/Manual dropdown and the clickable column headers. A header click
silently overrode Last visited with no way back and left the dropdown
showing a stale value.

Column sorting is now offered only when the active mode is Name — mirroring
how Manual already swaps in drag handles. Last visited and Manual (which
have no column equivalent) own the order; switching away from Name clears
any active column sort. Surfaces without a dropdown (the Add-accounts
picker, onboarding) keep columns sortable, as columns are their only sort
control there.
2026-07-20 08:04:09 +02:00

248 lines
12 KiB
TypeScript

import { defineConfig, loadEnv } from 'vite'
import { VitePWA } from 'vite-plugin-pwa'
import react from '@vitejs/plugin-react'
import tailwindcss from '@tailwindcss/vite'
import svgr from 'vite-plugin-svgr'
import mdx from '@mdx-js/rollup'
import remarkFrontmatter from 'remark-frontmatter'
import remarkMdxFrontmatter from 'remark-mdx-frontmatter'
import remarkHeadingId from 'remark-heading-id'
import remarkGfm from 'remark-gfm'
import path from 'node:path'
import { execSync } from 'node:child_process'
import { readFileSync } from 'node:fs'
import { swAssets } from './plugins/vite-plugin-sw-assets'
import { importMapIntegrity } from './plugins/vite-plugin-import-map-integrity'
const __dirname = path.dirname(new URL(import.meta.url).pathname)
const webRoot = path.resolve(__dirname, '../web')
const pkg = JSON.parse(readFileSync(path.resolve(webRoot, 'package.json'), 'utf-8'))
let commitHash = process.env.VITE_COMMIT_HASH || process.env.NEXT_PUBLIC_COMMIT_HASH || ''
if (!commitHash) {
try {
commitHash = execSync('git rev-parse --short HEAD').toString().trim()
} catch {
commitHash = ''
}
}
const appVersion = process.env.VISUAL_REGRESSION_BUILD === 'true' ? 'vistest' : pkg.version
if (process.env.VISUAL_REGRESSION_BUILD === 'true') commitHash = 'vistest'
// See decisions.md: env vars are renamed to VITE_* but a transitional shim
// re-exports them under their original NEXT_PUBLIC_* names so 99 existing
// call-sites can migrate incrementally. Build-time constants are injected here.
export default defineConfig(({ mode }) => {
// Vite normally exposes prefixed env vars on `import.meta.env`, but in
// production builds it leaves unknown `import.meta.env.X` references as
// runtime accesses against an object that doesn't exist — accessing
// `.NEXT_PUBLIC_SAFE_VERSION` then throws. To make the prod build behave
// like dev, gather every NEXT_PUBLIC_* / VITE_* value at build time and
// inline them as a literal `process.env` object via `define`.
const loadedEnv = loadEnv(mode, process.cwd(), ['VITE_', 'NEXT_PUBLIC_', 'EXPO_PUBLIC_'])
// `NODE_ENV` is typed as a narrow union, but the host shell can set it to
// 'cypress' (see comment below). Read it as a plain string so that check holds.
const hostNodeEnv = process.env.NODE_ENV as string
const processEnv = {
...loadedEnv,
// Build-time constants that override / supplement what loadEnv saw.
NEXT_PUBLIC_COMMIT_HASH: commitHash,
NEXT_PUBLIC_APP_VERSION: appVersion,
NEXT_PUBLIC_APP_HOMEPAGE: pkg.homepage ?? '',
// Honour an explicit cypress/test NODE_ENV from the host shell so the
// existing `cross-env NODE_ENV=cypress cypress run` script can flip
// `IS_TEST_E2E` in apps/web/src/config/constants.ts (and the require-login
// gate it gates) without needing a separate NEXT_PUBLIC_IS_TEST_E2E flag.
NODE_ENV:
hostNodeEnv === 'cypress' || hostNodeEnv === 'test'
? hostNodeEnv
: mode === 'production'
? 'production'
: 'development',
}
return {
publicDir: path.resolve(webRoot, 'public'),
envPrefix: ['VITE_', 'NEXT_PUBLIC_'],
server: {
port: 3000,
strictPort: true,
},
preview: {
port: 3000,
strictPort: true,
},
// Eagerly pre-bundle the heavy MUI / web3 / Redux subgraph. By default Vite
// discovers deps lazily on first import which produces hundreds of tiny
// chunks served individually in dev. Forcing inclusion collapses each top
// dep into a single optimized chunk.
optimizeDeps: {
include: [
'@mui/material',
'@mui/material/styles',
'@mui/icons-material',
'@mui/system',
'@emotion/react',
'@emotion/styled',
'@emotion/cache',
'react',
'react/jsx-runtime',
'react/jsx-dev-runtime',
'react-dom',
'react-dom/client',
'react-redux',
'react-helmet-async',
'@reduxjs/toolkit',
'@reduxjs/toolkit/query/react',
'@tanstack/react-router',
'lodash',
'date-fns',
'ethers',
'classnames',
],
},
define: {
__COMMIT_HASH__: JSON.stringify(commitHash),
__APP_VERSION__: JSON.stringify(appVersion),
__APP_HOMEPAGE__: JSON.stringify(pkg.homepage ?? ''),
// Build-time NEXT_PUBLIC_* vars that apps/web's next.config.mjs injects.
// Mirror the same set so reused source modules (e.g. config/version.ts)
// see real values at module-eval time.
'import.meta.env.NEXT_PUBLIC_COMMIT_HASH': JSON.stringify(commitHash),
'import.meta.env.NEXT_PUBLIC_APP_VERSION': JSON.stringify(appVersion),
'import.meta.env.NEXT_PUBLIC_APP_HOMEPAGE': JSON.stringify(pkg.homepage ?? ''),
// Reused code in apps/web/src and packages/* reads `process.env.NEXT_PUBLIC_*`
// at module top level. Vite has no `process` global in the browser, so the
// first such read throws synchronously and React never mounts. Inline an
// object literal containing every prefixed env var + the build-time
// constants — works in both dev and prod (unlike `import.meta.env`,
// which is undefined at runtime in prod builds).
'process.env': JSON.stringify(processEnv),
// Some web3 deps reference `global` directly (not just `globalThis`).
global: 'globalThis',
},
resolve: {
alias: [
// Next.js compatibility shims (decisions.md: cross-workspace next/* shimming)
{
find: 'next/dist/client/resolve-href',
replacement: path.resolve(__dirname, 'src/compat/next-resolve-href.ts'),
},
{ find: 'next/router', replacement: path.resolve(__dirname, 'src/compat/next-router.tsx') },
{ find: 'next/link', replacement: path.resolve(__dirname, 'src/compat/next-link.tsx') },
{ find: 'next/head', replacement: path.resolve(__dirname, 'src/compat/next-head.tsx') },
{ find: 'next/dynamic', replacement: path.resolve(__dirname, 'src/compat/next-dynamic.tsx') },
{ find: 'next/image', replacement: path.resolve(__dirname, 'src/compat/next-image.tsx') },
{ find: 'next/app', replacement: path.resolve(__dirname, 'src/compat/next-app.ts') },
{ find: 'next/navigation', replacement: path.resolve(__dirname, 'src/compat/next-navigation.tsx') },
// Node builtins referenced by reused source. `querystring-es3` is the
// standard browser polyfill webpack/Next uses internally, already in the lockfile.
{ find: /^querystring$/, replacement: 'querystring-es3' },
// Vite-native fork of the features/__core__ barrel. The original
// uses template-literal dynamic imports that Vite cannot analyze;
// this fork swaps in a Vite-native createFeatureHandle backed by
// import.meta.glob. Must appear BEFORE the broader `@/...` alias
// below so it takes precedence. Match the barrel exactly (no
// trailing segment) and also the direct module path for callers
// that bypass the barrel.
{
find: /^@\/features\/__core__$/,
replacement: path.resolve(__dirname, 'src/compat/features-core.ts'),
},
{
find: /^@\/features\/__core__\/createFeatureHandle$/,
replacement: path.resolve(__dirname, 'src/compat/createFeatureHandle.ts'),
},
// Cross-workspace source aliases — reuse apps/web/src verbatim during cutover.
{ find: /^@\/public\/(.*)$/, replacement: path.resolve(webRoot, 'public/$1') },
{ find: /^@\/(.*)$/, replacement: path.resolve(webRoot, 'src/$1') },
// Mirror apps/web/tsconfig.json `paths` so reused source resolves shared
// packages by subpath (e.g. `@safe-global/utils/utils/chains`).
{ find: /^@safe-global\/theme\/(.*)$/, replacement: path.resolve(__dirname, '../../packages/theme/src/$1') },
{ find: /^@safe-global\/store\/(.*)$/, replacement: path.resolve(__dirname, '../../packages/store/src/$1') },
{ find: /^@safe-global\/utils\/(.*)$/, replacement: path.resolve(__dirname, '../../packages/utils/src/$1') },
{ find: /^@safe-global\/test\/(.*)$/, replacement: path.resolve(__dirname, '../../config/test/$1') },
// apps/web/src/pages/_app.tsx uses bare `src/...` imports (Next.js baseUrl convention).
{ find: /^src\/(.*)$/, replacement: path.resolve(webRoot, 'src/$1') },
// Webpack aliases ported verbatim from apps/web/next.config.mjs.
{ find: /^@mui\/material$/, replacement: path.resolve(webRoot, 'src/components/common/Mui') },
{ find: 'bn.js', replacement: path.resolve(__dirname, '../../node_modules/bn.js/lib/bn.js') },
],
},
plugins: [
// Tailwind v4 — required for `@apply`, `@reference`, and all utility
// classes used both directly in JSX (`flex flex-wrap items-start ...`)
// and inside CSS modules (`@apply absolute top-0 right-0`). Without
// this, the entire shadcn/Tailwind layer is silently dropped and the
// PageLayout positioning collapses.
tailwindcss(),
// MDX support for .md/.mdx imports (terms, cookie, privacy pages).
// Mirrors the remark plugin list documented in
// docs/migration/state/plan.md and matches what apps/web's next.config.mjs
// uses through @mdx-js/loader. Must run BEFORE react() so React's
// JSX transform sees the compiled MDX output.
mdx({
remarkPlugins: [remarkFrontmatter, remarkMdxFrontmatter, remarkHeadingId, remarkGfm],
}),
react(),
svgr({
// Reused apps/web code does `import Icon from './x.svg'` and renders
// <Icon />. Webpack's @svgr/webpack returns a React component by
// default; vite-plugin-svgr returns the URL unless we opt in. Include
// every .svg and emit the component as the default export so the
// existing import sites work unchanged.
include: '**/*.svg',
svgrOptions: {
prettier: false,
svgo: false,
titleProp: true,
},
// Under Rolldown-powered Vite (v8) the plugin transpiles its SVG-as-JSX
// output via Vite's `transformWithOxc`. Without an explicit `jsx`
// option oxc parses but does not lower the JSX, so raw `<svg>` reaches
// the bundler and every .svg import fails with a parse error. Force the
// React 19 automatic runtime (same as @vitejs/plugin-react) so the JSX
// is lowered to `react/jsx-runtime` calls.
oxcOptions: {
jsx: { runtime: 'automatic' },
},
}),
// The worker is bundled by Vite, so the `define` block above (process.env,
// __APP_VERSION__) and the `@/` resolve aliases apply to it.
VitePWA({
strategies: 'injectManifest',
srcDir: 'src/service-worker',
filename: 'sw.ts',
registerType: 'prompt',
// PwaReloadPrompt's useRegisterSW performs the single registration; no
// competing auto-injected script.
injectRegister: false,
// Reuse the existing safe.webmanifest (linked via MetaTags); don't let
// the plugin generate/inject its own manifest.
manifest: false,
injectManifest: {
globPatterns: ['**/*.{js,css,html,svg,png,ico,webp,woff2}'],
// Exclude the stale publicDir service workers, the generated worker, and sourcemaps.
globIgnores: ['firebase-messaging-sw.js', 'workbox-*.js', 'sw.js', '**/*.map'],
maximumFileSizeToCacheInBytes: 5 * 1024 * 1024,
},
devOptions: {
enabled: true,
type: 'module',
navigateFallback: 'index.html',
},
}),
// Ordered after VitePWA so its cleanup runs once sw.js is generated.
swAssets(path.resolve(__dirname, 'src/service-worker/offline.html')),
importMapIntegrity(),
],
}
})