mirror of
https://github.com/luxfi/safe-wallet.git
synced 2026-10-30 15:19:21 +00:00
* fix(web): restore default body text colour for dark mode
The shadcn migration dropped MUI CssBaseline, which set `body { color }`.
Typography's default colour variant adds no class, so text without an
explicit colour inherited the browser default (black) — invisible on dark
backgrounds. Restore `color: var(--color-text-primary)` on html, body.
* refactor(web): unify table cards and tab styling
- Add a shared <TableCard> (borderless bg-card rounded-lg p-4 + in-card
table polish) as the single source of truth for the "table in a card"
look. Adopt it in the Address Book, Spaces Team list, and Spaces
contacts list, removing duplicated wrappers and the leftover border on
the Space address book.
- Add a `nav` variant to the shared Tabs component for top-level page
navigation; fix the underline that was clipped on Assets/Positions
(and other NavTabs pages) by positioning it flush to the bottom.
Slim NavTabs down to use the variant.
- Add space between the tab bar and the table on the Team and Spaces
contacts pages.
* fix(web): make TableCard tables render identically regardless of pagination
EnhancedTable carries its own standalone card chrome (rounded bottom when
not paginated, rounded footer + bottom margin when paginated). Inside a
TableCard this made a short table (e.g. Team) show a rounded last row while
a paginated one (e.g. Address Book) was square, with different bottom
spacing. Flatten the chrome inside TableCard so every table is square and
flush, paginated or not.
* chore(web): remove unused eslint-disable directives flagged after merge
The merge brought dev's stricter lint config, which flags the now-unnecessary
unused-imports/no-unused-vars directives in AddressInput/NameInput. Remove them.
* refactor(spaces): route Members table through the shared TableCard shell
The Members page hardcoded `bg-card rounded-lg p-4` and skipped the shared
in-card table polish (padded edges, no header divider, hover rows, borderless
last row), so its PaginatedDataTable rendered inconsistently with the Address
Book table which uses <TableCard>. Route it through TableCard so both Spaces
tables share one card shell and one source of truth.
* fix(web): match Apps filter bar background to page in dark mode
The Safe Apps search/category filter bar hardcoded
`background-color: var(--color-background-main)` (#121312), but since the
migration the dark-mode content background is `var(--background)` (#000, set on
.mainSpace app-wide). The opaque sticky bar therefore showed as a lighter strip.
Mirror the override already applied to SafeAppsHeader: keep
--color-background-main in light mode, switch to --background in dark.
* fix(web): add breathing room between Safe Apps section headers and card lists
Bump section header spacing (mt 24->32px, mb 16->24px) so titles like Featured
apps / All apps sit clearly apart from their card grids while staying grouped
with the content below.
* fix(spaces): make the Activity log borderless
The Activity list was the only Spaces list card with an outer border plus
colored row dividers, unlike Members/Address Book which use the borderless
TableCard look. Drop the outer border, the divide-y row separators, and the
load-more border-t; rows keep their py-3 spacing for separation.
* chore(web): fix storybook next/navigation mock and drop dead fetch-chains from dev
- alias next/navigation to a stub in .storybook so app-router hooks resolve
without disabling the pages-router mock (fixes NextjsRouterMocksNotAvailable)
- dev script: remove the deleted 'yarn fetch-chains &&' (merge artifact)
* fix(web): align Address book header with content and match Apps filter field heights
- Address book header px-4 -> px-6 so title/description/buttons line up with
the table card (which sits in a <main> that inherits the 24px content padding)
- Apps Category dropdown: force the shadcn Select trigger to 40px (!important)
so it matches the 40px SearchField; the trigger's data-[size=default]:h-9
otherwise out-specifies the module height
* chore(web): add Dialog and Calendar Storybook stories
Fill the two missing UI atom stories with their variants (Dialog: footer
actions / title-only / no-close; Calendar: single / range / multiple).
* chore(web): expand UI atom stories to cover all component variants
Audited each UI/* atom story against its component and added the missing
variants/states so Storybook mirrors the full component surface for designers:
alert (warning/success), badge (warning/success), button (aria-invalid/expanded),
avatar (fallback/badge/group-count icons), card (image-first), context-menu,
dropdown-menu, field, input-group, sidebar, select, tabs, toggle, switch,
navigation-menu, native-select, link, empty, typography, alert-dialog.
* chore(web): add PaginatedDataTable Storybook story
The shared Spaces table (Members + Address Book) had no isolated story. Cover
its variants: default (strong/sortable/aligned/secondary/sticky columns),
paginated, mobile row-detail, and empty.
* chore(web): detect stories/ subdir in coverage scanner + refresh report
The coverage scanner only looked for a colocated <name>.stories.tsx, so it
missed the entire components/ui/stories/ folder and reported ~50 storied atoms
as uncovered. Also check the sibling stories/ dir. Corrected coverage: 90%
(was a false 85%); story exports 632.
* chore(web): add Storybook stories for uncovered feature components
Add isolated stories (presentational or via the createMockStory harness) for 39
reusable feature/common components that had none: nested-safes, batching sidebar,
global-search, spending-limits, stake, wallet popover, assets list, safe-overview.
Skipped SpendingLimitsLoader (headless, renders null). Type-check, lint, prettier clean.
* chore(web): refresh Storybook coverage report
Reflects the new atom + feature-component stories: 94% components (1037/1109),
95% families, 92% groups, 698 story exports.
* fix(web): remove duplicate GlobalSearchModal story that broke the Storybook index
index.stories.tsx (barrel re-export) and GlobalSearchModal.stories.tsx shared
the same title, producing duplicate story IDs and failing the whole index build
(index.json 500). Keep the story on the real component; drop the barrel dup.
* fix(web): derive storybook next/navigation mock from the pages-router mock
The static next/navigation stub returned empty useParams/useSearchParams, so
hooks like useChainId (which reads useParams) resolved no safe/chain in stories
— making safe-scoped lookups (e.g. the batch slice) miss and render the empty
state. Derive usePathname/useSearchParams/useParams/useRouter from the
@storybook/nextjs pages-router mock that createMockStory already populates
(nextjs.router.query = { safe: 'eth:0x…' }). Verified: BatchSidebar
WithTransactions now renders its 3 rows; app-shell page stories still render.
* chore(web): normalize Storybook taxonomy + dedupe stories
Fold every story under the four canonical top-levels (Features/Components/UI/Pages):
- 27 stories retitled from stray groups (Dashboard/Common/Transactions/Settings/
SafeApps) and sentence-descriptions used as titles (e.g. 'Send tokens',
'This is a tooltip') into proper Group/Component paths.
- Fix 'Spending Limits'/'Nested Safes' spacing-casing to SpendingLimits/NestedSafes.
- Dedupe: remove duplicate NoSpendingLimits story (SpendingLimits.stories.tsx);
give EthHashInfo vs SrcEthHashInfo distinct titles (they are different components).
Result: 0 duplicate story titles; top-levels are Features/Components/UI/Pages only.
* chore(web): resolve Storybook taxonomy clashes + finalize scheme
- Split the clashing 'Pages/Features' into 'Pages/DeFi' (Swap/Bridge/Stake/Earn)
and 'Pages/Apps' — no more 'Features' appearing under both Pages and top-level.
- Fold vague 'Components/Base' into Components/Transactions and Components/Balances.
- Space-free feature groups: 'Actions Tray'->ActionsTray, 'Support Chat'->SupportChat.
- Order top-levels UI -> Components -> Features (design-system first) in storySort.
Scheme: UI = design-system atoms; Components = shared cross-feature; Features =
feature-scoped (by feature); Pages = full route stories (Core/DeFi/Apps/Onboarding/Spaces/Static).
* fix(web): finalize Storybook taxonomy + dashboard/topbar layout fixes
Storybook (designer-facing) hardening:
- Title 63 previously-untitled stories + repair 17 clobbered componentSubtitle/arg
values so the sidebar has exactly 4 canonical top-level groups (UI/Components/
Features/Pages) with no path-derived strays or duplicate IDs.
- Remove duplicate SafeShield mockup story (keep the real-component one), regroup
NFTs under Features, delete scaffold Configure.mdx, sort alphabetically within
groups, and document the taxonomy rules in .storybook/AGENTS.md.
- Delete unused CooldownButton (last consumer removed #3234); document SpaceCardNew
as staged Spaces-redesign work.
Real UI fixes surfaced via Storybook:
- PageLayout: publish the measured topbar height to --topbar-height via a callback-ref
ResizeObserver. The topbar wraps to ~148px below its @1100px container query but the
reserve was a fixed 100px, so the absolute topbar overlapped page content (dashboard
cards clipped). Now the reserve + all other --topbar-height consumers track reality.
- Overview: pin the refresh hint to the top and actions to the bottom via a stretched
justify-between column so they never overlap when actions wrap (keeps prior look).
- SafeAppsListHeader: render as a block <h2> so its bottom margin applies (was an inline
<span> whose margin was dropped, header touched the app grid).
- Add `block` to ~21 span-mapped Typography usages whose vertical margins were silently
ignored (inline elements drop mt/mb/my).
* fix(web): give signed-message list items the Queue card styling
Signed messages (Transactions → Messages) rendered as flat rows while Queue
items sit in cards. Reuse the shared transactions list-item card class
(`TxListItem/styles.module.css` `.listItem`) and match the trigger padding so
messages match Queue items in the list and on the single-message page.
* tests(web): populate the Messages page story with a signed-messages fixture
Add an MSW handler + MessagePage fixture (mixed CONFIRMED / NEEDS_CONFIRMATION,
date labels) to Pages/Core/Transactions/Messages so the list renders instead of
the empty placeholder, plus an Empty variant. Confirms the signed-message card
styling matches the Queue tab (light + dark).
* refactor(web): SettingsCard component + settings/primitive shadcn cleanup
Brings in the parallel shadcn-audit agent's work (uncommitted in the working
tree; see apps/web/docs/SHADCN_COMPONENT_AUDIT.md):
- Add reusable `settings/SettingsCard` (Card + h4 title + responsive grid) and
route ~13 settings sections through it (PushNotifications, DataManagement,
FeeTokenPreference, SafeAppsPermissions, SafeModules, SecuritySettings,
TransactionGuards, ProposersList, NestedSafesList, EnvironmentVariables,
FallbackHandler, SafeAppsSigningMethod) plus ChangeThreshold.
- P0 MUI cleanup in AddressBookInput + SpaceAddressBook clusters.
- P1 primitive fixes: button outline tokens + `render={<a/>}` link semantics,
alert warning token aliases (styles/shadcn.css), typography letter-spacing;
RecoveryCards, SpaceSettings, HnActivatedSettingsBanner surfaces.
- Broad new component test coverage across the above.
Verified: type-check, lint, prettier, and the new tests pass.
* refactor(web): add Button size=action + standardize app action buttons
Codify the ad-hoc "action pill" into the Button cva as size="action"
(h-10 px-6 gap-2, larger icon) and apply a consistent role→style mapping so
common actions stop hand-rolling padding/hover/min-width:
- ActionsTray + Spaces HeaderActions: Send=default, Receive/Swap/Build=secondary
/outline at size=action (drop px-6 / !px-6 / hover:bg-border overrides).
- Queue Confirm/Execute + message Sign: size=action for the prominent buttons,
compact rows stay sm; drop the magic min-w-[106.5px].
- Cookie banner Accept all (default) / Save settings (secondary) + settings Save
preference at size=action (drop min-w-[120px]).
- Transactions toolbar Filter/Export and Bulk execute use variant=outline: filled
secondary is invisible on the muted page background, so bordered on non-card
surfaces. Documented this surface rule in the Button story.
Button story documents the action CTA pattern as the designer reference.
Deferred (flagged): the slot-based tx-flow ComboSubmit/SplitMenuButton.
Verified: type-check, lint, prettier, tests; ActionsTray + tx toolbar checked in
Storybook (light + dark).
* fix(web): visible outline border in dark + filter Amount field height
- Button outline variant: use dark:border-border (#404040) instead of
dark:border-input (#ffffff0d, ~invisible) so bordered buttons (Filter, Export,
Bulk execute) are visible on the dark page background.
- TxFilterForm: the Amount NumberField carried the dead MUI-only inputCss.input
(height:66px on .MuiInputBase-root, a no-op on the shadcn field) so it fell back
to ~36px; use h-[66px] to match the Token address / date fields. Drop the unused
inputCss import.
Verified in Storybook: dark toolbar border visible; filter Amount = 66px = Token address.
* refactor(web): Button size=submit + destructive-outline; standardize money/action buttons
Second standardization pass, from a delegated consistency audit of ~180 button
files. Adds two Button primitives and applies them to remove ad-hoc overrides:
- size="submit": the action pill with a stable min-width for modal/flow submit
buttons (label can swap to a spinner without resizing). Replaces per-call magic
min-w-[82/112/114/200px]/[10em] across ReviewBatch, RecoveryAttemptReview,
CounterfactualForm, ProposerForm, ReviewTransactionContent, NftSendForm,
SpaceCreationModal.
- variant="destructive-outline": bordered destructive; replaces hand-rolled
outline + className="text-destructive" (DangerZoneSection "Leave workspace").
- size="action" for prominent toolbar CTAs previously on lg/sm: RejectTxButton,
ExecuteRecoveryButton (compact stays sm), AccountHeader "Manage Safe",
WidgetErrorState, WorkspaceHealthCard, AddContactDialog, Import.
- SplitMenuButton-based submits keep their own width; only the wrapper min-w was
dropped (SplitMenuButton takes no size).
- Redundant-override cleanups: HnCalendlyStep px-4, ApiCtaSidebar bg overrides,
EditMemberDialog wrong destructive variant on "Update".
Both new primitives documented in the Button story. Verified: type-check, lint,
prettier, tests; new variants render correctly in Storybook.
* refactor(web): resolve button design calls from sibling/surface evidence
- FeeTokenPreference "Save preference" -> default size (every other settings
save is default; the action pill was the outlier).
- AccountsHeader "Add" -> align to sibling CreateButton (size="sm", h-9, px-4).
- CreateSpaceOnboarding "Back" -> variant="secondary" (drop hand-rolled bg-muted),
keep the h-12 full-width footer sizing to match Next.
Left with justification: WcProposalForm equal-width pair, row delete icons.
* refactor(web): Input border token + inputSize variants; document variant-first styling
- Input: use the shared --border token (was hard-coded border-gray-100). NB --input is
#fff in light mode (invisible), so a visible field border must be border-border.
- Add inputSize (sm h-8 / default h-9 / lg h-10) mirroring SelectTrigger; named inputSize
(not size) to avoid the native numeric <input size> attr.
- Docs (DRY): Input story shows the sizes; a single "Component variants over custom styling"
section in .storybook/AGENTS.md is the reference; apps/web/AGENTS.md gets a pointer and its
stale "use MUI" principle replaced with shadcn/variant guidance.
* refactor(web): input/select consistency safe-fixes
From the delegated input-family audit — low-risk, mechanical:
- Search-field InputGroups (AccountsModal, SafeDropdownContainer) + SafeListSortToggle trigger: border-gray-100 -> border-border.
- ActivityLogFilters date field: border-input (invisible in light) -> border-border.
- CurrencySelect trigger dropped redundant h-8; MemberInfoForm role trigger dropped dead py-1.
- SidebarInput: h-8 literal -> inputSize=sm.
Verified: type-check, lint, prettier, tests. (Unsigned: 1Password agent was unresponsive.)
* refactor(web): InputGroup + SelectTrigger default to border-border (visible in light)
Root-cause fix: InputGroup and SelectTrigger defaulted to border-input, which is #fff in
light mode (invisible). Switch both to the --border token, matching the Input primitive.
Fixes every input-group search field (incl. AccountsSearch, which had no override) and every
select trigger app-wide in one place; removes the now-redundant per-call border-border patches
on AccountsModal + SafeDropdownContainer. Docs note updated.
* docs(web): design-system consistency status + next steps
Handoff for the variants-over-custom-styling effort: done (Button + Input/field primitives),
gotchas (--input white in light, inputSize naming, SplitMenuButton, 1Password signing flake),
and prioritized next steps (search-field unification, input audit tail, other families,
render-sweep harness, Chromatic). (Unsigned: 1Password agent unresponsive.)
* chore(web): seed mock data in tx detail stories + align Bulk execute button
The Tx (Detail) and Msg (MessageDetail) Storybook stories rendered an empty
skeleton: the detail pages read the tx id / message hash from the router
query, which the stories never provided (and Msg had no message-by-hash
handler). Seed a router query id (Detail) + message hash (MessageDetail), and
mock the messages list + by-hash endpoints so the message resolves via the
context-store list path.
Bulk execute: bump size sm -> action (h-10) so it matches the Filter/Export
toolbar buttons on the sibling History page (both render inside TxHeader). The
compact Confirm/Execute row actions are unchanged.
Also prettier-fix a stray markdown emphasis in DESIGN_SYSTEM_CONSISTENCY.md.
* fix(web): DS popover styling, white detail card, aligned msg confirmations
- Popover primitive: rounder corners (rounded-md -> rounded-lg), drop the 1px
ring border color, and use shadow-lg for edge definition — matches the
design-system popover look across all consumers.
- WalletInfo: "Switch wallet" default -> outline (the filled primary read as a
loud green in dark mode for a neutral utility action); Disconnect stays
destructive. Shared with the header AccountCenter popover.
- Transaction/message detail card: the expandable list item kept a green status
tint when open (data-open), lowering contrast on the detail content. It now
stays solid white/card with a neutral border when open; the :hover and
.batched (bulk-execute) highlights are unchanged.
- MsgDetails: the Confirmation 1/N accordions lacked the horizontal padding the
rest of the detail content has; added px-4 to match.
* fix(web): design-system audit fixes — tokens, primitives, product surfaces
Tokens: dark --destructive -> #ff5f72 and dark --accent-success -> #00b460 (brand palette
alignment; old destructive was sub-AA as text), dark --sidebar-accent was identical to the
sidebar bg (invisible active nav).
Primitives: border-input/border-gray-* -> border-border on checkbox/textarea/native-select/
toggle/combobox/input-otp/tabs; Base UI data-disabled variants (native disabled: never fired);
navigation-menu data-active fix; resizable retargeted to aria-orientation (rrp v4 dropped
data-panel-group-direction); progress no longer double-renders its track; calendar nav anchored
+ range styles; sidebar hsl(#hex) bug + floating ring; link rest-state underline; chip primary
solid; slider/switch/avatar/badge token fixes; dark disabled primary buttons read as muted.
Product: welcome watch-account button contrast, create/load/403/404 logo overlap, pending-txs
overflow, audit-log truncation, networks multiselect late-seed + placeholder, GlobalSearchInput/
SecuritySafesTable/SecurityReportDrawer/CurrencySelect token conformance.
* chore(web): storybook audit — hermetic mocks, open-state stories, mockup cleanup
- Pages/* stories are now hermetic: shared MSW handlers cover chains v2, indexing,
fiat codes, delegates, collectibles, owners/safes, safe overviews, zodiac-check and
WC telemetry (verified: 19 pages, zero external JSON requests under DNS blocking).
- Queue mock made internally coherent (executable tx at safe nonce, connected owner is
the missing signer) so Confirm/Execute render enabled states.
- Open-state stories for all overlay atoms (dialog, alert-dialog, drawer, dropdown,
hover-card, popover, sheet, tooltip, sonner, context-menu via right-click play fn).
- Fixed stories referencing @theme-only var(--color-*) names that do not exist at
runtime; Select stories pass Base UI items maps; missing StoreDecorators added.
- Story-mock gaps: address book seeded, spaces members DTO shape fixed, user settings
authenticated, welcome switcher pathnames set, dark canvas = #000 (token truth).
- Deleted 6 hand-built mockup story files superseded by real-component/page stories
(per .storybook/AGENTS.md); re-synced SpaceNestedSafesButton mockup markup.
* docs(web): design-system audit decision log
Records every decision applied in the audit (tokens, variants, story infra) plus the
remaining roadmap (11 mockup stories with invented copy, MSW tail, Chromatic).
* chore(web): promote the storybook render-sweep harness into scripts/storybook
yarn workspace @safe-global/web storybook:sweep [-- --filter=... --modes=... --shots=dir --json=out]
loads every story headless in light+dark and flags SB_ERROR / EMPTY_ROOT / BLANK_RENDER /
page errors — the harness used by the design-system audit, now runnable by CI/agents.
* chore(web): replace all 11 invented-copy mockup stories with real-component stories
Every remaining hand-built 'UI patterns' showcase (walletconnect, recovery, counterfactual,
nfts, proposers, notification-center, speedup, bridge, tx-notes, targeted-outreach,
no-fee-campaign) is replaced by stories that mount the real components with mocked data,
per the .storybook/AGENTS.md convention. 19 new story files; all verified rendering
non-empty in light + dark via the storybook:sweep harness before the mockups were deleted.
* fix(web): re-port dev logic lost in the migration; branch tests fully green
Re-ports (dev parity, shadcn primitives): GTF max-send fee banner in the token-transfer
RecipientRow; SpacesList require-login / AccountInfo header incl. signed-out and session-
expiry states; AddMemberModal invite payload/validation logic; PayNowPayLater multichain-
aware pay-method radio group; CreateButton passes the current page as the new-safe ?next
param again.
Test triage: updated stale copy/structure expectations across the 17 pre-existing failing
suites (GTF banner copy, balances tooltip, sentence-case sweep, shadcn DOM), 3 snapshots,
jsdom ResizeObserver polyfill in jest.setup (PageLayout topbar measurement).
verify:changed:web: 713/713 suites, 6397 tests, exit 0.
* docs(web): record the mockup-replacement + test-triage follow-up wave
* chore(web): Argos visual regression for Storybook
Builds the static Storybook, screenshots every story in light+dark via the
storybook:sweep harness (serving on port 4000 — whitelisted in OFFICIAL_HOSTS),
uploads to Argos, and fails on SB_ERROR stories. Mirrors the Cypress Argos E2E
workflow conventions; needs the ARGOS_TOKEN_STORYBOOK repo secret.
* chore(web): retire Chromatic in favor of Argos for Storybook visual regression
- Remove the Chromatic workflow, addon, CLI dep, script, and preview modes config.
- Introduce a positive 'skip-visual-test' story tag (reaches index.json, unlike the
Chromatic-era '!chromatic' negation) — the render-sweep harness render-checks tagged
stories but skips their Argos screenshots. Migrated all 60 '!chromatic' tags,
'chromatic: { disableSnapshot }' parameters, and per-story mode overrides.
- Document the Argos setup + opt-out tag in apps/web/AGENTS.md.
* chore(web): add missing keys to button story doc-table elements
* feat(web): button consistency — presets, variants & lint enforcement
Closes the design-system drift where call sites overrode <Button> via className
(the welcome/Accounts header was the trigger: sibling buttons all size="sm",
each overriding height/font/radius).
Presets (components/common/, closed APIs — take no styling className):
- SubmitButton, ActionBar/ActionButton, DialogActions, OnboardingFooter, IconAction
Primitive (components/ui/button.tsx):
- add `surface` variant (card-surface CTA) and `xl` size (48px onboarding footer)
Normalization:
- migrate the flagged screen + app-wide <Button> className overrides to
size/variant props; grandfather structural exceptions with justified
eslint-disables
Enforcement & docs:
- no-restricted-syntax ESLint rule: className is layout-only on Button + presets
- UI/Button -> Guidelines decision matrix; DESIGN_SYSTEM_CONSISTENCY playbook
* fix(web): re-port two dev display behaviours dropped in the shadcn migration
Audit of the flagged money-movement tx files (diff vs dev) found 3 real deltas,
all display-only (no amount/execution/signing/gas/relay-amount change). Two are
accidental drops from the UI rewrite and are restored to dev parity:
- ExecutionMethodSelector: restore dev's `isUnlimitedRelay` (GTF) gate so the
finite "N free transactions left" relay counter is hidden on unlimited-relay
chains instead of shown.
- TxDetails/Summary: restore the "Transaction hash" audit-log row and the GTF
HistoryFees breakdown accordion for executed txs (this file was their only
render site).
The 3rd (Receipt gas-token logo/tooltips) is a deliberate component redesign,
not an accidental drop — left as-is pending a design decision.
* feat(web): Tabs segmented variant + convert Accounts/Workspaces toggle to the primitive
- add a `segmented` TabsList variant (large pill toggle on a paper track) and
bake `cursor-pointer` into the TabsTrigger base (was repeated 7×)
- convert the bespoke AccountsNavigation CSS-module <Link> toggle (the welcome
Accounts/Workspaces switch) onto Tabs + the segmented variant, following the
NavTabs route-nav pattern; delete its styles.module.css. 1:1 visual port.
- drop redundant `h-auto` on two line-variant TabsLists and `px-4`/`cursor-pointer`
on the SecurityDrawer triggers
* refactor(web): roll DialogActions across 19 dialog footers
Migrate hand-built Cancel/Confirm dialog footers to the shared <DialogActions>
preset (unifies Cancel=outline + Confirm variants/sizes/spinner/layout). 19 clean
footers migrated (address book, members, spaces, trusted-safes, nested-safes,
batch-execute, cancel-recovery, ledger); 8 left as-is with reasons (wallet-gated
confirm via CheckWallet, confirm-tooltip, or not a cancel/confirm footer).
LeaveSpaceDialog test updated: confirm now shows the standard spinner while
loading instead of a bespoke "Leaving…" label.
* docs(web): document the Tabs `segmented` variant in the UI/Tabs story
* docs(web): design-system remaining-work backlog & parallel handoff
* feat(web): DialogActions wallet-gating + tooltip, migrate 4 more footers
Add confirmCheckWallet (wraps confirm in <CheckWallet>) and confirmTooltip
props to the DialogActions preset, and pin the Cancel button to type="button"
so it never submits a surrounding form. Route four more hand-built footers
through the preset: DeleteProposerDialog, UpsertProposer (both branches),
ImportAddressBookDialog, SafeListRemoveDialog (Track → inline trackEvent).
* docs(web): mark item A done + executable per-family sweep specs
Add docs/design-system/{card,dialog-drawer-sheet,badge-chip,input-inputgroup,select}-family-spec.md
— each has the exact cva additions, a per-file drift table, ESLint element names + regex caveat,
story additions, and design decisions to confirm. Update the backlog index to point at them.
* feat(web): SelectTrigger variant axis (surface/ghost) + lg size, migrate drift
Convert SelectTrigger skin to cva with a `variant` axis (default/surface/ghost) and add a
`lg` (h-10) size, keeping the data-size height mechanism so per-call `data-[size=…]:h-*`
overrides still resolve via tailwind-merge. default is byte-identical.
Migrate: ActivityLogFilters ×2 + MemberInviteRow → surface; AddManually → ghost.
CurrencySelect drops redundant classes (deliberate bg-background flat look grandfathered);
SafeSelectorDropdown's bespoke card-embedded trigger grandfathered. Extend the ESLint
no-restricted-syntax guard to <SelectTrigger>; document variants in the UI/Select story.
* docs(web): mark Select (C5) sweep done in the backlog
* feat(web): Badge/Chip size + shape axes, semantic colour parity (C3 part 1)
Extract geometry into `size` (sm/default/lg/auto) and `shape` (pill/tag) cva axes on
both Badge and Chip — defaults are byte-identical (Badge stays h-5, Chip stays
content-height). Add `info`/`positive`/`negative` to Badge and bring Chip to full
semantic-colour parity (warning/success/destructive/info/positive/negative). Add the
`--color-info-subtle/muted/strong` bridge in shadcn.css (mirrors success/warning).
Document the new axes in the UI/Badge and UI/Chip stories.
Non-breaking: existing call sites keep their look; the call-site drift migration +
ESLint guard land separately (some sites need a design nod on value-chip shades).
* docs(web): record Badge/Chip primitive (C3 part 1) done; migration pending design nod
* feat(web): finish Card family sweep — size="lg", radius default lg, ESLint guard
C1 Card family (part 1 + part 2):
- cva axes: variant (outlined/muted), size (sm/default/lg/none), radius (lg/xl/none)
- add size="lg" (gap-8/py-8, slot px-8); migrate the p-8 cluster via size="lg"+CardContent
- flip the default radius xl->lg app-wide (8px, MUI parity); drop TxCard's stale rounded-b-xl,
move TxDetails non-contrast branch xl->lg
- unify GlobalSearch gap-2->gap-4 via size="sm"
- migrate or grandfather every Card drift site; enable the Card-family ESLint guard
(dsCardClassnameRule for Card + slots + presets SettingsCard/SpaceSettingsSection/TxCard)
- document all axes + a layout-only guidance block in the UI/Card story
Needs Argos to confirm the app-wide radius flip.
* feat(web): Input/InputGroup xl size + surface variant, SearchInput preset (C4 part 1)
C4 Input/InputGroup family, part 1:
- input.tsx: add inputSize="xl" (66px) and a variant axis (default/surface); move
bg/px out of the base string so sizes/variants compose
- input-group.tsx: wrap in cva with matching inputSize (sm/default/lg/xl) + variant
- new components/ui/search-input.tsx: shared leading-icon search preset
- NumberField/NameInput pass through inputSize/variant (stop laundering 66px via className)
- migrate the safe search call sites and the 66px form/filter sites (TxFilterForm,
OwnerRow, SetAddressStep, DatePickerInput) to inputSize="xl"/variant="surface"
- stories + tests updated
Tail deferred (design-sensitive outliers + ESLint guard): NftGrid ghost filter,
SidebarInput bg-background, CreateSpaceOnboarding 44px, ActivityLogFilters rounded/color-scheme.
* feat(web): finish Input/InputGroup family — resolve outliers + ESLint guard (C4 part 2)
C4 Input/InputGroup tail:
- ActivityLogFilters, CreateSpaceOnboarding, MemberInviteRow → variant="surface"
(bespoke radius/44px height grandfathered)
- NftGrid borderless table filter, SidebarInput bg-background, InputGroupInput
primitive-internal stripping → justified eslint-disable grandfathers (single-use;
no one-off ghost/muted variant added)
- enable the Input-family ESLint guard (dsInputClassnameRule for Input/InputGroup* +
presets SearchField/SearchInput/NumberField/NameInput; button regex + border)
- backlog + spec marked C4 done
Follow-up: fully retire SearchField onto <SearchInput>.
* feat(web): unify on-colour CTAs onto Button variant="surface" (item B)
AddFundsBanner, EarnButton, and AccountHeader "Manage Safe" all sat on coloured
surfaces with hand-rolled paper/transparent backgrounds behind a justified
eslint-disable. Route them through the existing variant="surface" (bg-card + border
+ shadow, matching StakeButton) and delete the three disables.
Needs Argos to confirm the AccountHeader shift (transparent outline -> opaque surface).
* feat(web): migrate Badge/Chip call sites + enable ESLint guard (C3 part 2)
Take the deferred design calls and close the Badge/Chip family:
- migrate ~24 drift sites onto variant/size/shape props (accept the *-strong shade
shift; TxStatusChip + Warning move to a variant map + size="lg"; TxConfirmations
follows as backgroundColor is dropped)
- grandfather the genuinely bespoke sites (SafeGradeChip needs_attention, UnreadBadge
dot, FiatChange inline px-0); MUI Chip shim + ColorCodedTxAccordion get plain
comments (rule can't see inline style / css-module classes)
- delete 4 dead css-module classes (AccountItem .chip, BalanceChanges .categoryChip,
SafeAppTags .safeAppTagLabel, ExecutionMethodSelector .notAvailableChip)
- enable the Badge/Chip ESLint guard (dsBadgeClassnameRule; regex adds text-[…] + border)
- docs marked C3 done
Needs Argos to confirm the shade/size shifts. Follow-up: retire the MUI Chip shim.
* feat(web): Dialog/Sheet cva (size/padding/surface/divided) + ESLint guard (C2)
Close the Dialog/Drawer/Sheet family:
- DialogContent: cva size (MAX_WIDTH_MAP), padding (none/md, no default), surface
(default/card/paper); DialogHeader/Footer divided (+subtle). Defaults byte-identical.
- ModalDialog passes token widths via size= (arbitrary widths + fullScreen keep inline style).
- SheetContent: variant="floating", size, surface, padding; Header/Footer divided.
(Sheet size is a no-op for left/right sheets — base data-[side] specificity wins — so the
3 real sheets are unchanged; SecurityReportDrawer keeps w-[440px]! grandfathered.)
- Drawer unchanged; SafeAppPreviewDrawer radius grandfathered.
- migrate call sites to props; grandfather odd widths (420/440/560/700/960/423) + bespoke
paddings; delete redundant max-w-[500px]; enable the guard (dsDialogClassnameRule).
Needs Argos. Follow-up: make Sheet size functional (fold base widths into size default).
* docs(web): close item D — audit button disables, mark B/C2/C3/C4 done
Item D resolved: the on-colour CTA trio was removed via item B (variant="surface");
the remaining button eslint-disables are audited and kept as sanctioned escape hatches
(one-off/structural/stateful — building one-to-two-use Button variants would bloat the
primitive and mix concerns, contra the no-one-use-variant principle used across C1–C5).
Split-button is noted as a future ButtonGroup primitive. Backlog table updated: A, B,
C1–C5, D all done; only E (Argos secret) and F (manual QA) remain, both externally blocked.
* tests(web): cover new design-system variants + fix vacuous/stale tests
Adds real unit/render coverage for every new cva variant that shipped without a
test (found via an adversarial completeness audit):
- Button: surface, destructive-outline, size action/submit/xl + presets
SubmitButton/ActionBar+ActionButton/OnboardingFooter/IconAction/DialogActions
- SelectTrigger: surface, ghost, size lg; Tabs: segmented
- Card: size="sm", radius="xl"; Input: inputSize sm/default + variant default
- Badge/Chip: info/success/destructive + size lg (real toHaveClass assertions)
- Dialog/Sheet: size/padding/surface/divided/floating
Bug fixes surfaced by the audit:
- FiatChange.test: replace 5 vacuous MUI `toHaveStyle({'success.main'})` leftovers
(passed without verifying the migrated Chip) with real utility-class assertions
- TxStatusChip: delete the stale MuiChip storybook snapshot + add a proper unit test
asserting the migrated Badge variant mapping
+81 tests. type-check/eslint/prettier clean.
* chore(web): add Tabs ESLint guard + record post-migration audit follow-ups
- Add dsTabsClassnameRule (TabsList/TabsTrigger) — Tabs shipped the `segmented`
variant but had no guard; one bespoke gap-2 (SecurityDrawerContent) grandfathered.
All six family guards are now live; `eslint src` is 0 errors.
- Document the completeness-audit results in the backlog §6: variant test coverage
now done (+81 tests, 2 test bugs fixed), and the honest remaining follow-ups.
* fix(web): make SheetContent `size` functional for left/right sheets
`size` (sm/md/lg/auto) was a no-op for left/right sheets: the base string carried
`data-[side=left|right]:w-3/4` whose attribute-qualified specificity beat the plain
`w-*` the size variant emitted, so the size width never applied. This silently capped
BatchSidebar (size="lg", wanted 700px) and SideDrawer (size="auto") at the base width.
Fix: move the widths off the base and into `size` as `data-[side]`-scoped utilities
(matching specificity so they win), with `defaultVariants.size='sm'` preserving the
original w-3/4 default for sheets that set no size. Top/bottom keep full width via
`data-[side]:inset-x-0`. SecurityReportDrawer (grandfathered `w-[440px]!`) unchanged.
Updated sheet.test.tsx (asserts size now applies per side) + added a Sheet Sizes story.
Needs Argos to confirm BatchSidebar/SideDrawer pixel widths.
* refactor(web): de-drift element-level css-module skins onto guard-visible Tailwind
Move ~22 css-module skins (height/padding/radius/bg/border) off .module.css classes
onto the design-system primitive's className as pixel-identical arbitrary-value Tailwind,
so the ESLint guards can see them (each carries a justified eslint-disable). Deletes the
now-dead css and two whole modules (CounterfactualStatusButton, SpaceSidebarSelector).
Converted (all value-preserving — no pixel change): Card (OverviewWidget, SafeAppSocialLinksCard,
CardStepper no-op), Button (SendButton h-8 out of a template literal, CounterfactualStatusButton,
SafeAppsSDKLink, HnDashboardBanner, SpaceSidebarSelector, TokenMenu, DataManagement, HnSignupIntro,
BatchSidebar, WalletConnect x3), Select triggers (NetworkSelector, SafeAppsFilters, SignerSelector).
Left as documented follow-ups (genuinely unconvertible without a value change): MUI :global/child
overrides, runtime color-mix(), responsive @media, inline dynamic style, and the shared
AssetActionButton icon skin (DRY-encapsulated). Backlog §6 updated.
type-check/eslint(0 errors)/prettier clean; 6473 tests pass.
* docs(web): record css-module de-drift results in backlog §6
~22 element-level css-module skins ported to pixel-identical Tailwind (guard-visible);
remaining unconvertible ones (MUI :global, runtime color-mix, responsive @media, inline
dynamic style, shared AssetActionButton) listed as Argos-gated follow-ups.
* refactor(web): migrate StakingPromoBanner off MUI (merge fallout)
The dev merge brought a new StakingPromoBanner that still imported @mui/material
(Link) + @mui/icons-material (ArrowForward) — deps this branch removed, so a clean
CI install/build would fail (local type-check only passed on stale node_modules).
Port it like the rest of the migration: MUI Link -> ExternalLink (noIcon), MUI
ArrowForward -> lucide ArrowRight. apps/web/src now has zero @mui/@emotion code imports.
* fix(web): resolve shadcn migration regressions
* fix(web): support Next.js links in button render
* feat: add new transaction Storybook journey
* refactor(web): remove orphaned Spaces SearchInput wrapper
The feature-level SearchInput (features/spaces/components/SearchInput) had no
live importers — its former call sites (AddAccounts, SelectSafeModal/SafeSearch,
SafeSelectorDropdown, SelectSafesOnboarding) now use the shadcn primitive
@/components/ui/search-input directly. Only its own colocated test referenced it.
Verified dead via ripgrep across absolute/relative import paths, barrel
re-exports, default imports, dynamic imports, and raw string refs; type-check and
lint pass after removal.
* refactor(web): rename Input xl size token to hero
The xl size token meant two heights — Button size=action-scale xl (48px, OnboardingFooter) vs Input/InputGroup inputSize xl (66px, Safe creation / big filters). Rename the 66px Input tier to hero so one token name maps to one height; Button keeps xl. Renames the cva key in input.tsx + input-group.tsx and every inputSize=xl call site (consumers, tests, stories). Heights unchanged.
* refactor(web): use ActionButton preset for dashboard action-bar CTAs
The action tray built its Send/Receive/Swap/Build-transaction CTAs with raw <Button size=action>, bypassing the canonical ActionButton preset (which locks size=action). Swap the six action-bar CTAs to ActionButton and the hand-rolled flex/gap row to ActionBar. The icon-only build-tx button (size=icon-lg) is left unchanged.
* refactor(web): align TokenAmountInput adornment heights
Inside the hero (66px) amount field, the inline `Max` button is `sm`
(h-8) but the token `Select` trigger defaulted to `default` (h-9), so
the two end-adornments sat 4px apart and neither matched the h-8
vertical Separator between them. Give the token `SelectTrigger`
`size="sm"` so all three adornments share the 32px compact tier.
* refactor(web): drop dead Button/Select sizes and merge icon-lg
Trim variants/sizes that have no distinct usage so the scale stays
uniform (see docs/design-system-consolidation.md):
- Button: remove `xs` (0 real uses) and `icon-lg` (1 use). The lone
`icon-lg` site — the ActionsTray transaction-builder icon button —
moves to `icon` (36px), joining the default control row.
- SelectTrigger: remove the dead `lg` size (0 uses). `sm` stays — it is
the compact tier used by the TokenAmountInput adornment and the
CurrencySelect header picker.
Stories and the SelectTrigger height test are updated to the surviving
sizes.
* refactor(web): collapse Input/InputGroup to two sizes
Input and InputGroup drop `sm` and `lg`, keeping `default` (h-9) and
`hero` (66px) — the only two `inputSize` values with real usage. This
unifies the "one search box, one height" goal:
- SidebarInput (unused) and the SearchField preset drop their explicit
sizes, so every search box now sits on the default 36px row instead of
the SearchField-only 40px (`lg`) tier.
- SearchInput's `inputSize` narrows to `default`/`hero` as a consequence,
removing the divergence path without changing its call sites.
Stories and the size tests are updated to the two surviving sizes.
* refactor(web): merge redundant Button variants
Neither `destructive-outline` nor `link` carries distinct, recurring
usage that earns a variant, so both are removed (see
docs/design-system-consolidation.md):
- `destructive-outline` (2 sites, the DangerZoneSection "Leave
workspace" button) → `destructive`, the single destructive style.
Visible change: that button now reads as filled-tint rather than
bordered.
- `link` → the correct primitive per site: EditableApprovalItem's "Save"
is an action → `ghost`; WorkspaceBanner's external announcement link
is navigation → the existing `Link` component, which also sheds the
eslint-disabled button-height-stripping className hack.
Stories and the variant test follow suit (hover-card triggers → `ghost`,
alert-dialog trigger → `destructive`).
* refactor(web): replace NativeSelect with Select in EnhancedTable
The EnhancedTable rows-per-page picker was the only NativeSelect
consumer. Migrate it to the Base UI Select at `size="sm"` (matching the
compact pagination row), then delete `native-select.tsx` and its story —
one fewer dropdown primitive (see docs/design-system-consolidation.md).
Base UI's `onValueChange` is `(string | null)`, so the handler guards
null. The value is kept as a string (`items` maps value→label).
The legacy Cypress assets page object drove the picker via
`ul[role="listbox"]` (the pre-shadcn MUI dropdown, already stale against
the native select); it now targets the trigger by `data-testid` and the
options by `data-slot="select-item"`.
* refactor(web): use SelectTrigger ghost variant in NetworkSelector
Replace the hand-rolled stripped-trigger className
(`bg-transparent border-none shadow-none p-0`) with the SelectTrigger
`ghost` variant, so the stripped-trigger skin lives in one place (see
docs/design-system-consolidation.md). Only `h-full` (fills the header
row) remains as a documented per-call exception.
The variant is applied only when no `triggerClassName` override is
passed — SetAddressStep supplies its own full field skin, which stays on
the default variant, unchanged.
Note: in dark mode the default header trigger is now fully transparent.
The old hand-rolled strip only overrode the light background, so
`dark:bg-input/30` leaked through; `ghost` strips it in both modes.
* docs(web): add Storybook Design System section
A designer-facing "Design System" top-level Storybook group that shows
the shared, most-used building blocks (Button, Text Fields, Dropdowns,
Search, Tables, Cards, Tabs, Dialog, Tooltip) as they actually render,
each with "where it's used" links into the real screens — the alignment
surface between design and engineering. Adds a UI/SearchInput story.
- preview.tsx: the light/dark canvas now mirrors the card/paper surface
(#fff / #1c1c1c) so transparent controls (default Input, ghost Button)
are visible; storySort pins "Design System" above the dev-facing groups.
- docs/design-system-consolidation.md: the variant/size consolidation
proposal, cross-component harmony rules, and current landed status.
* refactor(web): collapse Tabs to underline + toggle variants
TabsList's four variants (default/line/nav/segmented) collapse to two
public families:
- `underline` — `tone="brand"` is the bold page nav (NavTabs), `tone="neutral"`
(default) the lighter in-content look (Spaces address book, members).
- `toggle` — `size="default"` the compact muted-track switch (SecurityHub
drawer, and the component default), `size="lg"` the large paper-track
welcome switch (AccountsNavigation).
Each (variant, tone|size) pair maps to one internal `look`, still emitted as
`data-variant`, so TabsTrigger's per-look styling is untouched and every
screen keeps its exact appearance — a faithful, zero-visible-change refactor
(design sign-off: preserve looks rather than unify them).
Consumers, stories and tests are migrated to the new API. This also folds in
a pending one-line segmented track-height fix that lives inside the rewritten
cva.
* docs(web): sync DS lint messages and agent guide with consolidated variants
Bring the guidance in line with the components after the consolidation:
- eslint.config.mjs — SelectTrigger `size` now ('sm'|'default'), Input
`inputSize` now ('default'|'hero'), Tabs `variant` now ('underline'|'toggle',
with tone/size). The selector regexes are unchanged.
- .storybook/AGENTS.md — Button drops `destructive-outline`/`link`/`xs`; Input
is `default`/`hero`.
- design-system-consolidation.md — Tabs section rewritten to the landed faithful
refactor; sequencing marks NativeSelect, NetworkSelector and Tabs as done.
* chore(web): checkpoint in-progress migration polish before dev merge
Pre-merge checkpoint so the tree is clean to merge dev. Groups the
in-flight shadcn work: CSS-module trims + component tweaks (NftGrid,
AssetsTable, SafeSelectorDropdown, PageLayout, tx-flow common, settings
lists), Storybook story updates, test additions/edits, and doc/config
updates (SHADCN_MIGRATION_ISSUES, AGENTS, launch.json, argos workflow).
Reversible via `git reset --soft` to reword or split.
* refactor(web): migrate SafeAccountsTable to shadcn Table
Dev's #8271 added SafeAccountsTable in MUI (Table/TableCell/Box/TableSortLabel +
useForkRef); the dev merge pulled it onto this branch, reintroducing MUI. Port it to
the shadcn Table primitive so the branch is 0-MUI again:
- MUI Table/TableBody/TableCell/TableHead/TableRow/TableContainer/Box → the
@/components/ui/table sub-components, rendered inside a raw <table> so the reorder
grip that floats in the gutter isn't clipped by the primitive's overflow-x-auto container.
- TableSortLabel → a role="button" sort header with lucide chevrons + aria-sort (matches
EnhancedTable's pattern).
- The former `sx` (grey header bar, inset hover pills, gradient row divider) → a colocated
styles.module.css, in the spirit of EnhancedTable/cardTable.module.css.
- useForkRef → a small useCallback ref-merge (React 19 ref-as-prop on TableRow/TableBody).
- @hello-pangea/dnd drag-reorder logic unchanged.
Zero @mui/@emotion left in apps/web/src. type-check + lint clean; 226 consumer tests
(AccountsWidget, AddAccounts, SafeAccounts, onboarding, TrustedSafesModal, MyAccountsV2) pass.
* fix(web): stop Storybook TDZ crash from myAccounts↔spaces import cycle
The myAccounts and spaces feature barrels are mutually dependent (a large,
app-tolerated import cycle spanning the store). webpack's React Refresh runtime
eagerly reads every export at module-eval time, so a `const`/arrow export read
mid-cycle hits its TDZ and crashes Storybook with "Cannot access before
initialization". Next.js (Rspack) never eagerly reads exports, so the app was
unaffected.
Convert the three cross-feature-cycle exports to hoisted `function`
declarations, which webpack hoists above the module's requires and can read
even while the module is still initializing:
- SafeAccountsTable: inline `export default function` (a trailing
`export default X` compiles to a TDZ `__WEBPACK_DEFAULT_EXPORT__` temp)
- AccountItem: hoisted function namespace (only ever used as AccountItem.X)
- useMergedAddressBooks: `export function`
Behavior-preserving; no barrel/loading changes. Verified all four SCC entry
points render in Storybook (SafeAccountsTable, AccountItem, AccountsWidget,
pages/spaces/safe-accounts) and the app is unchanged. type-check, lint,
prettier, 6636 tests pass.
* fix(web): restore white light-mode sidebar and align accounts nav tabs
Two visual corrections from post-dev-merge review:
- Sidebar: the light-mode `--sidebar` token pointed at --color-background-main
(the grey page bg), making the sidebar blend edgelessly into the page (it was
white before the migration). Point it at --color-background-paper (white) so
it reads as a distinct panel again. Dark mode is unchanged (still the page
background, which is correct there).
- Accounts nav tabs: the Workspaces/My accounts TabsList carried
`w-full max-w-[440px]`, but its <Tabs> parent shrank to content inside the
centered flex wrapper, so the tabs rendered at ~355px instead of 440px. Move
the width constraint to the <Tabs> root so the tab bar matches the 440px
Get started / login card width and aligns with it.
Verified in Storybook + app: light sidebar #ffffff vs #f4f4f4 page, dark
unchanged; tabs and card both 440px, same left edge.
* fix(web): match welcome tabs to reference design and give the sidebar a visible border
Visual-parity fixes against the pre-migration (staging) reference, which is the
source of truth for the look:
- Welcome Accounts/Workspaces tabs (segmented toggle variant): the migration
diverged from the reference — text-xl bold labels, a paper track, content-width
(flex-none) pills, and a grey "secondary" active pill. Restore the reference
look: muted track, text-lg normal-weight idle labels, flex-1 pills that stretch
to fill each half (so it reads as a toggle), and a raised paper
(bg-background + shadow-sm) active pill; drop the radius overrides so the base
16px track / 12px pill apply and nest cleanly. Only consumer is
AccountsNavigation. Updated the colocated unit test + story/comment.
- Spaces sidebar divider: the sidebar relied on the floating variant's ring
(a box-shadow) for its edge, which is low-contrast and gets clipped by
overflow-hidden ancestors in the app — so it read as having no border. Swap
it for a real border-r using --sidebar-border (adapts per theme; drop the
ring to avoid a doubled line). Mirrored on the loading skeleton.
Verified light + dark in Storybook and the app (triggers 214px each = exact
halves); type-check, lint, prettier, 6636 tests pass.
* fix: avoid nested anchor in SafeAccountsTable row navigation
The row's NextLink wrapped the whole SafeInfoDisplay, which renders the
ExplorerLinkButton's <a> (plus copy/rename controls) — nesting an <a>
inside the row <a>, which is invalid HTML and triggers a hydration error.
Scope the navigation link to the name text only (new SafeInfoDisplay
`nameLink` prop) so the explorer/copy/rename controls become siblings of
the link instead of descendants. Whole-row click navigation, keyboard
focus, and the clip-gated name/address tooltips are preserved.
(cherry picked from commit cf7506f25805ba9608d52640586c373e27e89748)
* docs(web): correct the Select sm-size note in the design-system story
The note claimed sm is "only for the compact currency selector", but sm Select
is used in three compact inline contexts — the currency selector, the
rows-per-page pagination control, and the token select inside amount inputs.
Name all three so the story matches actual usage.
* refactor(web): collapse the Select to one height and drop the surface variant
Consolidate the dropdown API per design review:
- Remove the `sm` size. The trigger now has a single height — `min-h-9` instead
of a fixed `h-9`/`h-8` — so ordinary one-line selects are unchanged (~36px)
but a rich multi-line value (the token picker's icon + name + balance) grows
to fit with padding instead of being squished into 32px. Removed the `size`
prop; kept a hardcoded `data-size="default"` so the existing
`data-[size=default]:h-*` height-override hook still resolves.
- Remove the `surface` variant. Its 3 users (activity-log filters ×2, invite
member role select) move to `default`, which reads as consistent bordered
fields alongside the date inputs they sit with.
Migrated consumers: TokenAmountInput, CurrencySelect, EnhancedTable
(rows-per-page), SafeSelectorDropdown, MemberInviteRow, ActivityLogFilters.
Updated the UI/Select + design-system stories (the dropdowns story now shows
the token picker growing to fit rich content) and the select unit tests.
Verified in Storybook: token picker 54px (padded, no longer cramped), currency
select + activity-log filters render cleanly. type-check, lint, prettier, 6638
tests pass.
* fix(web): make button intent consistent — Save size, Add funds icon, icon rule
From the Buttons design-review pass:
- Space Settings Save/Cancel dropped size="sm" → default, so the primary
save/confirm action matches every other save/confirm in the app instead of
being an odd compact one-off (IdentitySection).
- Dashboard "Add funds" gains a leading + icon, consistent with the other
standalone add/create action ("Add proposer +").
- Buttons story: show Export with its real icon (it always had one — the story
didn't), correct the Save swatch (default, not sm), and document the icon
rule — leading icon only for standalone verb actions (money movement,
add/create, export); dialog & form-footer actions (confirm/cancel/save/delete)
stay text-only.
Verified in Storybook; type-check, lint, prettier, 6638 tests pass.
* fix(web): size Space Settings danger-zone buttons to default (not sm)
Leave/Delete workspace were size="sm" (h-8) — too small for destructive
actions and inconsistent with the Save/Cancel above them (now default h-9).
Bump all three danger-zone destructive buttons to the default size. The
Buttons story already shows Delete at default, so it now matches reality.
sm stays as the compact/inline size everywhere else (75 uses across 34 areas —
menus, bars, load-more, inline row actions); this is a per-button fix, not a
size-system change.
type-check, lint, prettier, 6638 tests pass.
* docs(web): surface design-review questions in each design-system story
Add a "To review with design" callout to every DS family page (via a new
`review` prop on the Family helper) so the open questions + when-to-use
suggestions are in front of us during the designer session — no separate doc to
carry. Each family leads with the when-to-use rule for its important composites,
then the consolidation questions from the review (e.g. Buttons: which preset
when + collapse the ghost icon-buttons; Dropdowns: Select vs DropdownMenu vs
Combobox + dead context-menu; Cards: the radius scale that forces raw-div
cards). Keeps the DS pages to the important shapes + guidance — the exhaustive
per-variant coverage stays in the UI/ pages.
Story-only; type-check, lint, prettier, 6638 tests pass.
* docs(web): put the Buttons review questions in-context, next to each component
Following review feedback that a floating block of questions is hard to map to
the actual buttons: give Swatch optional `use` (when-to-use caption) and
`review` (inline open question) props, and attach the Buttons questions to the
components they're about instead of a top text block:
- Render CopyButton + ExplorerButton beside IconAction so the "these three are
all ghost icon-sm — collapse into one preset?" question is visible in context.
- when-to-use captions on each preset/composite (ActionButton, SubmitButton,
IconAction, DialogActions, OnboardingFooter, ChoiceButton, SplitMenuButton).
- ChoiceButton "rebuild on the primitive?" on its swatch; the lg/action/submit
size question on the lg swatch; the stale-variant note in "Aim for uniformity".
Verified the story renders (CopyButton + ExplorerButton included). type-check +
lint clean.
* docs(web): rework the Tables story around table types, not the primitive
Per review feedback that the raw Table primitive isn't the point — we ship many
table types and need to decide which to use when (and whether to unify them):
- Lead reframed to "pick a table type"; the primitive is demoted to a "shared
shell" link.
- Render EnhancedTable live as the default grid, with the unify question
attached in-context (is PaginatedDataTable its successor? ~9 consumers; align
pagination).
- "Which table for what" catalog (new CatalogRow helper) for the types too
heavy / feature-scoped to mock here — PaginatedDataTable, SafeAccountsTable,
AssetsTable, NftGrid, and the key/value DataTable — each with when-to-use, a
link to its own story, and its open question (unify, NftGrid CSS borrow,
DataTable rename).
PaginatedDataTable is cataloged (linked) rather than imported, to respect the
feature-import boundary. Story-only; type-check + lint clean.
* docs(web): show EnhancedTable in its TableCard wrapper, as the app renders it
The Tables story rendered EnhancedTable bare on Storybook's white canvas,
which is not how any table looks in the app — every grid sits inside a card
surface. Wrap the demo in TableCard (the canonical 'table inside a card'
wrapper) on the grey page background so the card reads as a card, and add
TableCard to the 'which table for what' catalog with its own open question.
* fix(web): stack tx-flow layout vertically on narrow screens
Below 900px the tx flow used an absolute-positioning treatment that broke:
the title shrank to 16px and overlapped the progress bar, and the Safe
Shield ("copilot") widget was pushed behind the card (z-index: -1) so it
disappeared entirely. The MUI->shadcn migration had also dropped the
coordinating .progressBar rule, and the JSX used Tailwind's md (768px)
breakpoint while the CSS module and useIsBelowMd used 900px, leaving a
broken 768-900px dead zone.
Replace the absolute hacks with a clean vertical stack — title above the
card at full h3 size, card, then the Safe Shield widget full-width below —
and unify the side-by-side breakpoint on 900px so the layout switches in
one place instead of two mismatched ones. Applied to both TxLayout and its
near-identical twin TxFlowContent (which the newer slot-based flows render
through). Desktop (>=900px) side-by-side + status rail is unchanged.
* refactor(web): merge duplicate tx-flow layout wrappers into TxLayoutBase
TxLayout and TxFlowContent were ~95% duplicated: the same status rail +
titled card + progress + Safe Shield sidebar markup, and two copies of the
same styles.module.css. That duplication is why the earlier responsive fix
had to be applied twice and was easy to miss.
Extract the shared chrome (and TxLayoutHeader) into a new presentational
TxLayoutBase with a single stylesheet. The two entry points become thin
adapters with unchanged public APIs:
- TxLayout: prop-driven, owns the tx providers, computes step progress
- TxFlowContent: context-driven (TxFlowContext), passes the sidebar slot
No consumer changes; every flow keeps rendering the same markup. Adds a
TxLayoutBase unit test (title/step, widget + rail visibility, hideSafeShield,
isReplacement, sidebar slot, back-button) — the first coverage for this
shared surface.
* fix(web): stop the nonce dropdown crashing and discarding a typed nonce
Two defects in the same JSX block, both introduced when TxNonce moved from MUI
Autocomplete to the Base UI combobox.
`ComboboxLabel` is Base UI's `Combobox.GroupLabel`, which throws without a
`Combobox.Group` ancestor. Both labels were rendered directly inside
`ComboboxList`, so `useComboboxGroupContext()` threw the moment the popup
mounted — and since the recommended nonce is always `options[0]`, every open
path hit it. TxNonce is rendered from TxLayoutBase, so clicking the nonce field
on any review screen was a full-page error boundary.
Separately, selection was controlled only via `inputValue`. Base UI's
`handleUnmount` resets the input to `stringifyAsLabel(selectedValue)` when the
popup closes; with selection uncontrolled that resolves to `''`, silently
discarding a typed nonce. The reset is not gated on `hasInputValue`, so passing
`inputValue` alone does not protect against it.
Split the list into two real groups (`items` on `ComboboxGroup`, render function
in `ComboboxCollection`) and bound `value`/`onValueChange` to the form field.
Adds regression tests for both defects and the component's first Storybook
story — the absence of one is a fair part of why this shipped.
* fix(web): keep the edited approval amount when the preset list closes
Same root cause as the nonce field: the approval-amount combobox was controlled
via `inputValue` with selection left uncontrolled, so Base UI's close handler
reset the input to the empty selected value.
The consequence here is worse than a cleared field. A wiped sibling row makes
`updateApprovalTxs` throw silently, so the row keeps displaying the amount the
user typed while the `safeTx` they are about to sign still encodes the dApp's
original — often unlimited — allowance. The screen and the payload disagree.
Binding `value` also fixes a second symptom: Base UI's default single-selection
filter is built from `selectedValue`, so with it null the typed amount was
treated as a search query and the sole "Unlimited amount" preset was filtered
out — the dropdown opened empty. One fix clears both.
Adds the component's first test file and Storybook story.
* fix(web): dismiss the address-book suggestion list on escape, outside click and blur
MUI's Autocomplete closed its listbox on Escape, on an outside click and when
focus left the field. The hand-rolled replacement kept none of the three, so the
suggestion panel stayed open at `--z-overlay` across the rest of the form and the
only way to dismiss it was to click an option — which writes that contact in as
the recipient.
Across the nine address-entry forms that render this (send tokens, send NFTs,
add owner, …) a user who clicks into the recipient field and then reaches for the
amount field hits the panel instead, and that click silently substitutes an
address-book contact as the recipient.
Restores all three paths with a document-level pointerdown/keydown listener plus
a wrapper-scoped focusout, matching the pattern already used in this branch by
NetworkMultiSelectorInput. Options `preventDefault` on mousedown to hold focus,
so selecting one never fires focusout and still works.
Adds regression tests for each dismissal path and the component's first
Storybook story.
* fix(web): stop the sidebar logo tripping React's hydration check
`showHomeLabel` was decided on the first render from two client-only inputs: the
safe address (a query param the server cannot see during SSG, which is why
useSafeAddressFromUrl falls back to `location.search`) and the sidebar's
collapsed state (a cookie read on mount).
So the server emitted the plain-logo variant and the browser's first pass emitted
the "Home" pill, and React reported a recoverable hydration error on SafeLogo —
"server rendered HTML didn't match the client".
Gates the variant on the existing `useSidebarHydrated`, already used by
SpacesEnhancedSidebar and SafeSidebarVariant for exactly this. Both passes now
render the plain logo and the pill appears after mount.
This is pre-existing on dev, not a shadcn-migration regression — every component
and hook in the reported stack is byte-identical there. Committed separately so
it can be dropped or cherry-picked independently of the migration work.
* fix(web): keep disabled anchor buttons inert
Button's anchor fast-path clones the render element directly instead of going
through ButtonPrimitive, and cast the remaining props to anchor props. That cast
laundered the button-only `disabled` and `focusableWhenDisabled` onto an <a>,
where `disabled` is inert: no aria-disabled, no tabIndex management, onClick
still fired, and the cva base string's `disabled:`/`aria-disabled:` styling hooks
never matched, so the element painted as a live full-opacity CTA. MUI's
ButtonBase used to cover this by setting aria-disabled and tabIndex={-1} on the
anchor it swapped in.
Destructure both button-only props out of the rest spread and, when disabled,
mirror base-ui's non-native button contract on the anchor (aria-disabled,
tabIndex={-1}, preventDefault) instead. The href is kept so the element keeps its
link role. The enabled path produces an identical props object.
Track's shouldTrack only looked for `*[disabled]`, so the bogus attribute was the
only thing suppressing analytics for a disabled anchor; widen it to aria-disabled
so the event stays suppressed for both the anchor and render={<span/>} paths.
The visible symptom: the "Go to Nested Safe" CTA on the tx success screen was
clickable while a nested-Safe deployment was still processing, tearing down the
status modal mid-deployment.
* fix(web): report a Tenderly call-trace error as a failed simulation again
The shadcn/Tailwind migration added a new first-priority branch to the two
queued-simulation display helpers, so a transaction whose Tenderly call trace
carried an error rendered an amber warning icon and "Can execute (with
warnings)". Before the migration this was a strict subtraction: anything other
than a clean success was red "Simulation failed".
That made the queue row the only surface in the monorepo calling a call-trace
error executable. The shared classifier in packages/utils
(getSimulationOutcome's mainIsSuccess), web Safe Shield and mobile
SafeShieldWidget all still treat it as a failure, so the same signer saw
"Can execute" in the queue and "Simulation failed" in the execute flow.
isCallTraceError is reachable: getCallTraceErrors filters the flat call_trace
array for any entry carrying an error at any depth, so a swallowed inner revert
sets it on an otherwise successful tx, and a legacy (<1.3.0) Safe with a
non-zero safeTxGas can emit ExecutionFailure while the outer tx succeeds and
the gas is spent.
Restore the pre-migration semantics locally and collapse the duplicated
condition into a single predicate so the icon and the label cannot drift apart
again. packages/utils is untouched, so mobile is unaffected. Whether a benign
swallowed revert deserves its own third state is a product decision that would
have to change the shared classifier, web Safe Shield and mobile together.
* fix(web): restore the invalid state on the advanced Safe creation fields
MUI's TextField took two props: a boolean `error` that drove the red
border and label off the mere existence of a form error, and a separate
`helperText` for the message. The shadcn `Input` collapses both into a
single `error?: string`, so the payment receiver lost its only working
signal: a bare `required: true` makes react-hook-form emit an empty
message, `'' ?? fallback` keeps the empty string, and `!!''` leaves
aria-invalid unset and the message paragraph unrendered. Clearing the
field produced no visual or a11y feedback at all while the Next button
went quietly dead.
The rules now carry real messages, the invalid state is read off the
Controller's `fieldState` and pushed onto both `aria-invalid` and the
Field's `data-invalid` so it no longer depends on a non-empty string,
and `??` becomes `||` so an empty message falls through to the
already-deployed hint instead of masking it. The salt nonce kept its
boolean `error` prop and stayed red, but had the same swallowed
messages.
* fix(web): reject a malformed payment receiver on the advanced creation step
The paymentReceiver Controller carried only a `required` rule, so any non-empty
string kept formState.isValid true and left the Next button enabled. Typing
`foo` then sent a garbage receiver into createNewUndeployedSafeWithoutSalt ->
predictAddressBasedOnReplayData, which throws inside useAsync; that error is
discarded, so the "New Safe address" skeleton spun forever with no explanation
while the user carried on through the flow.
Adds `validate: validateAddress` to the rules. That helper is the same one
AddressInput already validates through (validatePrefixedAddress delegates to it),
so every user-entered address in the app is checksum-strict and this field is now
consistent rather than newly strict.
validateAddress also rejects a non-checksummed address, and the step's default
comes from NEXT_PUBLIC_ECOSYSTEM_ID_ADDRESS, which is injected per environment
and set in no file in this repo. A lowercase value would have opened the step
already-invalid with Next dead, so the default is now checksummed at the point of
use — checksumAddress passes non-addresses through untouched, so a misconfigured
env var still surfaces as a validation error instead of throwing.
Pre-existing on dev, not a shadcn-migration regression. Complements 070bdd261,
which restored the invalid state for the *empty* field; this covers a malformed
one.
* fix(web): lift the private key dialog above the wallet-connect modal
The shadcn migration dropped the explicit z-index from PkModulePopup, so the
dialog and its scrim inherited z-[var(--z-overlay)] (1400) from
dialogContentVariants. onboard.css deliberately puts the wallet-connect modal at
1450, and that modal is still on screen when the private key is requested — so
the popup painted underneath onboard's opaque, pointer-capturing scrim and every
click landed on onboard instead of the dialog.
Re-adding the old `sx` prop would have been a silent no-op: ModalDialog declares
`sx` but never applies it. Instead DialogContent now accepts an optional
`overlayClassName` (mirroring SheetContent), ModalDialog forwards it, and the
popup asks for the new --z-above-onboard token. Both the popup and its own scrim
have to clear 1450 — moving only the popup would leave onboard's scrim eating
outside clicks and dismissing the wrong modal.
--z-overlay stays at 1400 on purpose: onboard must remain above ordinary dialogs
so a wallet connect launched from one stacks on top.
* fix(web): stop the Safe Apps queue bar hiding behind its own backdrop
The shadcn migration replaced MUI's <Backdrop>, which carried no z-index at
all, with a scrim at z-[var(--z-overlay)] (1400). That is above the queue
bar's own 1200, so an expanded queue rendered dimmed and inert inside the
Safe Apps viewer: the first click on a transaction row or on "Batch execute"
hit the scrim and collapsed the bar instead.
The scrim now lives next to .barWrapper in the colocated CSS module at
z-index 1199, so the two coupled values sit in one file. Click-away collapse
is unchanged — the scrim's onClick is what replaced dev's ClickAwayListener,
so it cannot simply be dropped.
* refactor(web): align Input/Select/Button heights and settle the field skins
Design-system pass carried over from an earlier session, committed as-is.
Input gains `sm` (h-8) and `lg` (h-10) alongside `default` (h-9) and `hero`, so a
field can be matched to the button beside it — the heights now mirror Button's
`sm`/`default`/`lg` scale rather than being chosen ad hoc per call site. The same
scale is added to SelectTrigger, and the ESLint guard messages are updated to
name the new sizes so the rule teaches the current API.
The default Input skin becomes `bg-input` (the filled field) instead of
`bg-transparent dark:bg-input/30`, and Button's `outline` variant becomes
`bg-transparent` with a translucent `foreground` hover tint — `--muted` (#f5f5f5)
sits one value from the page background (#f4f4f4), so a surface-coloured hover was
invisible on exactly the surfaces that variant is for. InputGroup gains a `search`
variant, and the surrounding call sites drop their hand-rolled height/skin
classNames in favour of the props.
Validated before committing rather than reviewed line by line: type-check clean,
ESLint clean over the touched components, 810 tests across components/ui,
components/common and SafeAccounts green.
* fix(web): stop a caller's aria-invalid erasing Input's own invalid state
Input computed `aria-invalid` from three signals — the script-injection guard, the
`error` prop, and the caller's own value — and then spread `{...props}` on the very
next line. Because `aria-invalid` was never destructured out, the spread overwrote
the computed attribute with the raw prop.
So `<Input error="Boom" />` worked (no such key in props) but
`<Input error="Boom" aria-invalid={undefined} />` silently dropped the attribute,
and `aria-invalid={false}` rendered `aria-invalid="false"` — either way erasing an
invalid state the error or the injection guard had already established, along with
the destructive ring and border the cva string hangs off it.
Destructures `'aria-invalid': ariaInvalid` so the OR of all three signals wins
regardless of prop order.
AdvancedOptionsStep passes both `error` and `aria-invalid` and was correct only
because both derived from the same boolean; it now holds by construction. The
sibling primitives are unaffected — input-group, textarea and select only style
off `aria-invalid` via CSS selectors and never compute it.
Pre-existing on dev, not a shadcn-migration regression.
* fix(gtf): restore the fees panel controls deleted in the shadcn migration
The migration cut FeesPreview from 395 lines to 71, leaving a single gas-fee row.
`useFeesPreview` was untouched and still computed everything, so the data layer
went on resolving a payment mode and a gas token that no UI displayed:
`canCoverFees` went from 6 uses to 0, `totalOutgoing` from 8 to 0, and
`setGtfPaymentMode` lost its only production call site.
That mattered because the payment mode is read from a persisted preference and
still rewrites the signed payload through mergeGtfFeeParams — a signer on a GTF
chain was signing a Safe-pays transaction with nothing on screen saying so, no way
to switch, no total, and no warning when the Safe could not cover the fee.
Rebuilds the five deleted pieces in shadcn against the state that already exists:
the pay-fees-from selector (SafeTxContext), the gas-token selector
(availableGasTokens / onGasTokenChange), the execution-fee row, Total outgoing,
and the low-balance Alert. MUI Popover + MenuItem become DropdownMenu; Divider
becomes Separator. The locked gas-token row renders without a trigger, since
signer-pays always uses the native token and an interactive control would imply
otherwise.
Also drops the local FeeRow duplicate in favour of `shared/FeeBreakdownRow`, which
was already ported and handles isFree, note, fiat, loading and error.
dev's 188-line test file is restored unmodified and all 21 of its assertions pass
against the shadcn rebuild — it is the spec, not a rewrite. The story gains the
store/router decorators the panel now needs and holds the payment mode in state so
the selector is actually operable.
* fix(spaces): open the safe selector when its name or balance is clicked
SelectTrigger renders as `absolute inset-0 z-0`, a sibling of — not an ancestor of
— the display layer above it at `relative z-10 … pointer-events-none` with
`pointer-events-auto` restored on tooltip triggers. The safe name and the balance
are tooltip triggers, so a press on either landed on a <span> the trigger behind it
never saw. Hit-testing across the row in a browser: 5%, 30%, 50% and 70% opened the
popup; 15% (the name) and 90% (the balance) did nothing.
Forwards the press to the trigger via a ref, skipping it when the press hit one of
the row's own controls (explorer link, copy button) or when base-ui had already
dismissed an open popup during the same pointerdown — otherwise forwarding would
reopen what the user just closed. The remembered open state is captured in the
capture phase because base-ui flushes the closed state mid-pointerdown, so by the
bubble phase the guard would read stale.
Tooltips keep their pointer events, so hover still works: the click continues to
land on the span and is delegated on from there.
Started by an agent whose session was interrupted; finished, linted and verified
here. Its new test fails without the source change ("forwards a click on inert
trigger content to the select trigger"), and all six hit-test positions now open
the popup.
* fix(web): restore legible text on the semantic tint variants
The migration introduced `--color-*-strong` aliases pointing at `--color-*-dark`
and used them as foreground text on `--color-*-subtle` backgrounds. But those
tokens hold the SAME value in both themes — they are accents, not inks — so as
text each one fails AA in at least one theme:
info light 2.04:1 ✗ dark 7.33:1
warning light 4.38:1 dark 3.14:1 ✗
success light 3.99:1 ✗ dark 3.65:1 ✗
dev did not do this. `AccountItemStatusChip` on dev set only a background
(`--color-info-light` / `--color-warning-background`) and left the text at the
theme default, which measures ~16.4:1. The migration tinted the ink as well, and
that is what broke it — the pill reading "Activating account" renders #52bfdc on
#effcff in light mode.
Restores dev's model across the tint variants: the background and the icon carry
the meaning, the text stays `text-foreground`. Same tints, same borders, same
icon colours — only the body ink changes back. That puts every variant at
13.8–18.9:1. The icon keeps `--color-*-strong`, where 4.38/3.14:1 clears the 3:1
threshold that applies to graphics rather than text.
Touches chip, badge and alert, plus WidgetViewAll which applied the same pairing
by hand. Six class-string assertions across five test files updated to the
restored contract.
Verified by computing the ratios from the resolved token values in vars.css, not
by eye — Storybook would not build in a usable time. Worth a visual pass in both
themes before release.
* fix(web): restore the MUI breakpoints the migration replaced with 768px
Seven components were switched onto `useIsMobile` from `@/hooks/use-mobile` —
shadcn's stock hook at 768px — during the single squashed migration commit. Six of
them were on MUI's `sm` (600px) before, so their layout now flips 168px earlier
than it was designed to:
AssetsTable, BlockedAddress, Breadcrumbs/BreadcrumbItem, EthHashInfo,
ModalDialog, SafesList/SafeListItem
The visible effects were every dialog going full-screen on tablet-portrait and
zoomed desktop, and `EthHashInfo` shortening addresses on 600–767px even where the
caller passed `shortAddress={false}` — including the address-poisoning warning
dialog, which asks the user to verify an address it was truncating.
PushNotifications was a different substitution, not a threshold shift: dev used
`breakpoints.up('lg')` and shortened addresses below 1200px, while `useIsMobile`
shortened below 768px — so between those widths it showed a full address where dev
showed a short one. Restored to the `lg` condition.
Adds `SM_DOWN_QUERY`/`useIsBelowSm` and `LG_UP_QUERY`/`useIsAboveLg` alongside the
existing `md` pair, and pins all three values in a test — this drifted silently
once because the numbers lived only in call sites.
The five components that were already on `useIsMobile` in dev (PaginatedDataTable,
MembersList, the two SpaceAddressBook files, ui/sidebar) are deliberately left at
768px.
* fix(web): let keyboard users expand multi-chain account groups
Base UI's trigger defaults to `nativeButton: true`. When rendered as something
other than a <button> that default keeps the native-button code path, so no
`role="button"` and no Enter/Space handling are applied. MultiAccountItem renders
its CollapsibleTrigger as a div, so the multi-chain group header on the accounts
list could be tabbed to but never activated — every Safe inside a group was
unreachable without a mouse. It worked on dev via MUI's ButtonBase, which
synthesises a click from Enter and Space.
Fixed with `nativeButton={false}`, one prop.
Covered at the primitive rather than the call site: MultiAccountItem needs the
feature loader, analytics and two data hooks to mount, whereas the broken contract
is CollapsibleTrigger's, and the same trap exists wherever a trigger is rendered as
a div (RecoveryListItem and SingleTxDecoded use the same shape). The new test
asserts role and both key activations, and pins the default's behaviour so the trap
is documented rather than rediscovered.
* fix(web): name the icon-only tooltip buttons for screen readers
MUI v6's Tooltip copied a string `title` onto its child as an `aria-label`
(Tooltip.js:544, with `describeChild` false by default), so an icon-only button
wrapped in one got its accessible name for free. Base UI's Tooltip wires no ARIA at
all — its only role is `presentation` on the positioner — so those buttons now
announce as bare "button".
Adds an explicit `aria-label` to the six that were left unnamed:
TxSigners copy transaction hash (enabled and disabled states)
TxSigners copy transaction link
TxSigners view on block explorer (disabled fallback)
GasLimitInput reset to recommended gas limit
AccountItemPinButton add to / remove from my accounts
NestedSafesPopover manage nested Safes
Found by matching the shape rather than by eye — an icon-only Button or IconButton
inside a TooltipTrigger with no aria-label — and the sweep now reports none. It does
miss triggers wrapped in an intermediate element, which is how the disabled explorer
button was found by hand afterwards.
* tests(web): drop the Tailwind class-string assertions
Removes 188 of 209 `toHaveClass` assertions. They could not fail for the right
reason: jsdom loads no CSS, so `expect(el).toHaveClass('h-10')` only proves the
string is in the class attribute — not that anything is 40px tall. They pass even
when a class is never emitted, and they break on every design-system rename. That
happened twice in this branch's own history: six of them broke on the contrast fix
purely because it changed `text-warning-strong` to `text-foreground`, and the only
way to "fix" them was to paste the new name in.
Kept 21 where the class is a marker rather than the subject — the test passes an
arbitrary class in and checks it reached the right slot, usually paired with a
`not.toHaveClass` on the sibling. That is prop forwarding, and it is real: the
`z-[1451]`/`z-[1452]` pair in dialog.test.tsx is what proves `className` and
`overlayClassName` do not cross over, which is what the private-key dialog's
stacking fix depends on. Also kept CSS-module class checks, which assert a
component's own styling hook rather than a utility.
108 whole tests went with them, wherever the remaining body no longer proved what
the title claimed — a render-only test under "applies the info variant classes" is
worse than none. Seven files emptied out entirely and were removed: ui/sheet,
ui/alert, ui/input-group, common/ActionBar, common/NameInput,
common/NumberField/index.render, common/ProgressBar.
Adds styles/semanticContrast.test.ts in place of the alert/chip/badge class checks.
It reads the token values out of vars.css and shadcn.css and computes the actual
WCAG ratio for the three tint surfaces in both themes, so it fails on the thing the
class assertion was standing in for. Verified by temporarily setting light
`--foreground` to the low-contrast accent: three tests go red, and green again on
revert.
Two coverage gaps worth naming rather than burying:
- ui/sheet.test.tsx was entirely class assertions, including the only guard that
SheetContent still emits `data-closed:pointer-events-none` — the fix for an
invisible click-swallowing backdrop. Protecting that needs a real click-through
test, not a class string.
- TxCard.test.tsx was left untouched. Its `'txCardActions', '!mt-0'` pair reads as
pass-through through `cn()` (the base hook is a global styling anchor that
TxLayoutBase targets), but it is a borderline call and the file has no other test.
* fix(web): re-open the action required panel when the vulnerability check resolves
ActionRequiredPanel read `defaultExpanded` only in the `useState` initialiser,
so the prop was captured once on mount. The dashboard derives it from
`useVulnerableSafe`, an async Zodiac check that returns false until it resolves,
which meant a Safe flagged as vulnerable kept the panel collapsed forever and
VulnerableModuleWarning was only reachable by clicking the header.
Restore the open-once effect the migration dropped: state starts collapsed, an
effect on `defaultExpanded` opens it, and a `userToggledRef` guard makes a manual
toggle win so a late or again-true signal never re-opens a panel the user closed.
The ref is also set from the Collapsible `onOpenChange` handler so a Base
UI-driven change cannot be overridden. Restores the prop's JSDoc contract.
Adds the two rerender-based tests neither branch had: the async false -> true
flip must expand, and a user collapse must survive a later flip back to true.
* fix(web): read media queries synchronously on the first render
useMediaQuery started at `false` and only corrected itself in a passive
effect, so every consumer painted the desktop branch for one frame before
snapping to the small-screen one — most visibly ModalDialog (fullscreen on
phones), TxLayoutBase, SideDrawer, Topbar and EthHashInfo's address
truncation. MUI's useMediaQuery, which this hook replaced, reads the
MediaQueryList through useSyncExternalStore and was therefore right on the
first render of any component mounted after hydration.
Rewritten on useSyncExternalStore with a `false` server snapshot, so the
value is read during render on the client and hydration still matches the
server. matchMedia is optional-chained, so a runtime without it yields
`false` instead of throwing. The exported queries and tier hooks are
unchanged, so no call site moves.
* fix(web): repair focus rings, scroll locking and overlay z-order
Fallout from the shadcn migration across the shared form fields, dialogs
and overlays:
- The shared large-form-field classes carried `focus-visible:*` rules. They
are merged over a primitive's own classes with tailwind-merge, so they
silently replaced them — SelectTrigger's 3px `ring-ring/50` halo became a
1px `ring-ring` with the border held at its resting colour, which is
invisible in dark mode (`--ring` and `--border` are both #404040).
- The body-scroll guard matched Base UI overlays by `data-slot` alone. Under
`keepMounted` (CaptchaModal) the node stays in the DOM and is only marked
`hidden`, so a closed overlay kept matching and disabled the guard for the
rest of the session. Now scoped with `:not([hidden])`.
- Added `--z-nested-overlay` for a dialog opened from inside another overlay
(Rename from a modal or an open dropdown); they share a portal container,
so at equal z-index the winner was whichever mounted last.
- Dropped the dead MUI dialog rules left behind in the ModalDialog and
TxModalDialog stylesheets.
Colocated tests added for the touched components and the two stylesheets.
* fix(web): restore keyboard access to accordion rows and disabled tooltips
More shadcn migration fallout, all in transaction lists:
- Accordion triggers that host their own action slots cannot be a native
<button>, so they were rendered as a `div` with `role="button"` and a
tabindex. Base UI only installs its Enter/Space fallback when it is told
the element is not a button, so those rows could be focused but not
activated from the keyboard. Pass `nativeButton={false}` and drop the
hand-rolled role/tabindex.
- The copy-hash hint hung off a disabled Button. A disabled button receives
neither pointer nor focus events, so the tooltip was unreachable by every
input method. Moved onto a focusable wrapper.
- ExpandableTransactionItem mounts its details on first expand and keeps
them mounted afterwards. A constant `keepMounted` mounted every row's
TxDetails upfront, one details request per row in the list.
* fix(web): let WalletConnect negotiate a session and show its QR modal
Two independent faults made WalletConnect unusable:
- The QR modal's `--wcm-z-index` (1302) sat below onboard's
`--onboard-modal-z-index` (1450). Since the QR modal opens from inside
onboard's connect modal, onboard's "Connecting to WalletConnect…" panel
painted over the QR code and the connection could never be completed.
Raised to 1451, matching shadcn.css's `--z-above-onboard`.
- `requiredChains` put the Safe's chain in WalletConnect's
`requiredNamespaces`, which a wallet cannot negotiate. A wallet that
cannot serve that chain — MetaMask with test networks off, against a
Sepolia Safe — showed its approval sheet and then silently never returned
a session, so the connect hung with nothing logged on either side.
Verified from the WalletConnect store: `requiredNamespaces` held
`eip155:11155111` with no session. Dropping it also leaves
`optionalChains` at every chain onboard was initialised with, so the
wallet connects with whatever it supports.
Being on the wrong chain afterwards was already handled: `useIsWrongChain`
gates the UI and `assertWalletChain` switches the wallet before signing.
Adds walletModalZIndex.test.ts, which ties the three z-indexes together —
they live in two stylesheets and a JS theme variable with nothing else
connecting them.
* fix(web): align the topbar and tx-flow chrome with the page layout
The migration moved the topbar to `position: absolute` with `.mainSpace`
reserving `padding-top: var(--topbar-height)`, but left several rules
written for the old in-flow topbar. All measured in the browser:
- `.topbarElevated` stayed `position: sticky`, which is in flow, so while a
tx-flow or recovery modal was open the topbar landed *after* its own
reserve — an empty band of page equal to its height above it (88px at
desktop, 152px once the header wraps), and the tx dialog's close button
buried underneath it, unreachable per `elementFromPoint`. With the sidebar
visible its `left: 0` / `width: 100%` also resolved against `.main`, so the
header content was offset by the sidebar twice (x=484px). Now `fixed`, out
of flow like the base variant, and returned to `sticky` below md where
nothing is reserved.
- The topbar's sidebar offset now resolves through `--topbar-offset`, so the
collapsed and no-sidebar states actually reach the elevated variant; they
were dead CSS, leaving the header stuck at 230px while the content moved.
- TxModalDialog hard-coded `top: 148px` for 900–1148px. The measured height
there is 152px, so the dialog's top edge tucked back under the topbar.
`--topbar-height` is measured live, so the banded override is gone.
- The tx dialog painted `--background` over `.paper` — a white sheet on the
grey page in light mode. It now tracks the page in both themes; the two
tokens diverge in dark (#121312 vs #000000), so both halves are pinned.
- TxStatusWidget's step icons mask the connector line behind them with an
opaque background. It was pinned to the old white surface and showed as a
white block per step; retargeted to the dialog surface in both themes.
- The tx-flow row had no gutter, so the status rail sat at x=0 against the
viewport edge. Added `--space-3`, matching the topbar's own `px-6`.
- The logo variant of the topbar opted out of the wide-content wrapping, so
on routes with no Safe the 24px mark stays left and the actions stay right
at every width. The safe-selector chips gained a `min-h-10` floor — they
borrowed their height from a sibling via `self-stretch` and collapsed once
the selector wrapped — and the chips now sit below the address on mobile.
- The safe selector's chevron sat 2px from the pill edge against the network
dropdown's 8px; its trigger is `absolute inset-0` in a `-m-4` wrapper, so
`pr-2.5` was almost entirely spent covering the overshoot.
- Removed the dead `.mainSpaceCompact` rule: nothing applies it anywhere in
the monorepo, and the fixed reserve it held is now measured at runtime.
Covered by source-reading tests for the cross-file couplings behind these —
the dialog offset against the measured topbar height, both background pairs
against the page, the elevated topbar staying out of flow, and the chip
heights against the selector. jsdom loads no CSS, so nothing else catches
them.
* fix(web): restore the gas-token logo and symbol on the tx receipt
The shadcn migration dropped the token identity from the GasToken row: the
`heldToken` / `gasTokenLogo` / `gasTokenSymbol` derivation was deleted and the
`EthHashInfo` switched from `showAvatar={false}` plus a `TokenIcon` child to a
generic address identicon. The row then showed only a bare address, which says
nothing about what is actually being spent — on a GTF Safe-pays transaction
that address is the token the Safe pays fees in.
Re-derives the logo/symbol from the native currency or the held ERC-20 and
renders the icon beside the address again, with the explanatory tooltip, in
shadcn form. Behaviour matches dev; only the markup is Tailwind now.
Covered by three tests (native, held ERC-20, and an ERC-20 the Safe does not
hold). Verified they fail without the fix rather than passing over the gap.
* fix(web): repair the web-tanstack type-check the migration broke
`Web TanStack Checks` is green on dev and has been failing on this branch:
apps/web-tanstack compiles ../web/src through its own tsconfig, so the
migration's changes reach it without apps/web's own checks noticing —
web-checks.yml type-checks only --filter=@safe-global/web. The failure blocks
CI through `verify.mjs --changed --workspace=web-tanstack`, which runs
type-check; `vite build` alone never caught it because esbuild strips types
without checking them.
Two causes, 17 errors:
1. The branch deleted apps/web/src/utils/createEmotionCache, which
__root.tsx still imported (its `@/*` alias points at ../web/src). That
file mirrors _app.tsx, and _app.tsx no longer sets up Emotion or MUI, so
the CacheProvider/CssBaseline wrapper is dead wiring rather than something
to restore: neither apps/web/src nor apps/web-tanstack/src has a single
@mui or @emotion import left. Removed it.
2. Sixteen icon-prop errors. apps/web/src/definitions.d.ts types the `*.svg`
default export as `any`; apps/web-tanstack/src/svg.d.ts types it with
SVGR's real signature, whose index signature makes it unassignable to
`ComponentType<SVGProps<SVGSVGElement>>`. The narrow annotation therefore
passes in one project and fails in the other. Added a shared SvgrComponent
type matching SVGR's shape and used it in the three annotation sites
(CircularIcon, StatusLabel, StakingStatus). Left svg.d.ts alone — its index
signature is deliberate and documented.
SvgrComponent lives under components/common/icons rather than src/types,
which is gitignored for generated contract types.
Verified: type-check now passes for all 7 workspaces (first time on this
branch), web 804 suites / 6902 tests, lint 0 errors, prettier. Also ran the
web-tanstack CI steps directly: build, assert:sri (585 assets), and the four
service-worker assertions.
* tests(web): regenerate the Storybook DOM snapshots for the shadcn markup
243 of 248 snapshots were stale against the migrated components, failing
`storybook-snapshots` in CI. Regenerated rather than hand-edited, but audited
the diff first so this isn't a green suite over a real loss:
- The churn is overwhelmingly `class="…"` (MUI/Emotion class names to Tailwind)
and MUI artifacts disappearing: auto-generated `data-testid="*Icon"`,
`aria-hidden` and `tabindex` on decorative icons.
- Base UI semantics are added, not lost: role +270, aria-invalid +254,
aria-disabled +202, disabled +238, aria-expanded +180.
- Of the 44 distinct `data-testid`s the diff removes, every one is a MUI
auto-generated `*Icon` id. No project test id was dropped.
- The removed `aria-label`s are MUI Tooltip's `title`-to-`aria-label`
injection. Spot-checked the icon-only controls behind them: CopyButton
still sets `aria-label` explicitly (and is asserted via
getByRole(name: 'Copy address')), 'Refresh positions data' is live tooltip
text with its own test, and 'More information about this feature' is a
Storybook arg, not a control label.
Verified: `yarn test:storybook:ci` — 67 suites / 250 tests / 248 snapshots pass.
Only .snap files change; no source is touched.
* tests(web): retarget Cypress selectors at the Base UI control shapes
Six smoke specs fail on this branch but pass on dev. The Cypress Cloud errors
are element-selector timeouts, not missing test ids — every test id these specs
touch still exists. Three distinct shape changes:
- Base UI's Switch renders `<span role="switch" aria-checked>`; it emits a
native `input[type="checkbox"]` only when given a `name`, and even then it is
visually hidden. `toggleShowAllTokens` / `toggleHideDust` searched for that
input inside the switch. They now read `aria-checked` and click the switch
element itself, which is where the test id already sits. Fixes assets,
balances_endpoints and tokens.
- AddressBookInput and NumberField spread their props onto the underlying
input, so `[data-testid="beneficiary-section"]` and
`[data-testid="token-amount-field"]` ARE the inputs. Dropped the
`.find('input')` hops that looked for a descendant.
- AddressInput's FieldLabel is a sibling of the input inside the Field wrapper,
so the error assertion goes through `closest('[data-slot="field"]')`.
Verified the Switch/Checkbox DOM by rendering the primitives rather than
assuming. Not verified end-to-end: the smoke specs need
CYPRESS_WALLET_CREDENTIALS, a CI-only secret, so CI has to confirm these.
add_owner's `[data-testid="address-item"]` failure is untouched — that one
needs the address-book suggestion flow looked at separately.
* fix(web): correct tx surface, radius and layout regressions from the shadcn migration
Visual fixes found while reviewing the transaction screens:
- `--muted` was doing double duty as a recessed surface, but it sits darker
than `--card` in light mode and 2.3x brighter in dark, so expanded tx
details read as raised in one theme and sunken in the other. Added
`--surface-sunken` (pinned to the page floor) for the bulk/conflict group
wells, and made the expanded detail panel plain `--card`, matching
ExpandableMsgItem and the pre-migration behaviour.
- Restored the two-row Send/To layout in TransferTxInfo: bold labels in a
40px column, un-bolded amount, 32px token icon, no inline trailing "to".
- NetworkWarning lost its icon and gained a border; restored the warning
triangle, the lighter `--color-warning-background` fill and a transparent
border.
- SafeShield radii were not concentric: both inner sections used 16px inside
a 24px outer with a 4px inset. Outer is now 16px and the inner sections
12px. The old comment claimed `rounded-b-lg` was 8px, which is untrue in
this theme.
- Dropped the tx-flow card outline (header border + TxCard `outlined`), which
MUI's Paper never had. Form-control and error borders are untouched.
- The status rail was a fixed 200px at every width above 900px, squeezing the
card until the header wrapped mid-word. It now collapses to icons only
below 1200px (labels stay in the a11y tree via `sr-only`) and the middle
column gives back its 80px of padding.
* refactor(web): replace the tx-flow discard confirm with an AlertDialog
Closing a tx flow was gated on `window.confirm`. Browsers can suppress that
dialog (embedded webviews, "prevent additional dialogs"), and a suppressed
confirm reads as false, so `handleModalClose` bailed out with no feedback and
the close button looked dead. Not a migration regression - the confirm dates
back to 35548fadc - but it fails silently and does not match our own dialogs.
`usePreventNavigation` needs a synchronous answer: `preventDefault` on a
mousedown and a `false` from `beforePopState` are only honoured in the same
tick, so a modal cannot be awaited in place. It now takes `(proceed) =>
boolean` and hands over the navigation it blocked; `requestDiscard` returns
false to block immediately, parks the action, and the dialog runs it on
confirm. Confirming a blocked navigation therefore still completes it.
Also moves the supersede check out of the `setFlow` updater, where `confirm`
would have fired twice under StrictMode, and uses `--z-nested-overlay` since
the tx dialog is z-index 1300 below 900px and would paint over the default
z-50.
Covers the gate with tests: opt-out closes silently, warn keeps the flow
mounted until answered, cancel keeps it, confirm closes it, and the flow's own
onClose runs only after confirming.
* fix(web): use the 24px card radius in tx flows and stack the Hypernative banner
Corrects the radius in b2f236215, which lowered the tx-flow header to 16px on
the assumption that Card's `lg` default was the app standard. Measuring the
dashboard shows every card computes to 24px, so the actual defect was the
bottom being too flat, not the top being too round: TxCard now passes
`radius="xl"` and the header goes back to `rounded-t-xl`, giving 24px at both
ends.
Also on the New transaction landing view:
- Added `overflow-hidden` so the square-ended ProgressBar is clipped to the
corner instead of jutting past the rounded top-left, matching what
TxLayoutBase's header already did.
- Dropped `border border-border`, which the other tx-flow surfaces no longer
carry.
HnMiniTxBanner rendered "securityLearn more" run together: both Typography
children are inline spans and `.bannerContent` was not a column, so they laid
out inline. The description's existing `padding-top` shows stacking was always
the intent.
* fix(web): keep the split button's dropdown half inside the row
The chevron half of the Execute button sat outside the button group, where
TxCard's overflow-hidden clipped it away entirely.
Cause was the primary half's `minWidth: ${maxCharLen}ch`. That floor keeps the
button from resizing as the selected option's label changes, but it is derived
from the longest label and took no account of the row it has to fit in: at
24ch inside a 220px group it measured 233px on its own and pushed the 42px
dropdown 55px past the right edge. Capping it against the space left after
that half - `min(${maxCharLen}ch, 100% - 3rem)`, where 3rem is the dropdown's
max-w-12 - keeps both halves inside at 200px, 400px and 800px. `min-w-0
flex-1` replaces `w-full` so the primary can actually yield that space.
Also flattens the seam: Button's base carries
`in-data-[slot=button-group]:rounded-md`, and that variant outranks a bare
`rounded-r-none`, so both halves kept their 12px inner corners and met on two
curves rather than joining. Prefixing the corner resets with the same variant
evens up specificity - measured 0px inner, 12px outer.
* fix(web): warning alert colour, queue row wrap and two overflow fixes
- Alert `warning` now uses `--color-warning-background` with a coral
`--color-warning-main` icon and no border, instead of the amber
`warning1` scale plus `border-warning-muted`. Moves the treatment
NetworkWarning was carrying as a one-off into the variant, so every warning
alert matches, and drops that call site's override.
- Queue rows must wrap below 1350px: eight columns need ~863px and the row is
654px at a 1000px viewport. The wrap left every cell packed to the left,
losing the column order the grid conveys. The confirmations cell now takes
`margin-left: auto` so the confirmations/status/actions cluster stays against
the right edge.
- ExecutionMethodSelector clips to its own 16px radius, so the relay counter's
6px bottom corners stop poking outside it.
- Removes the vertical separator between "Expand all" and "Collapse all", plus
its now-unused import and `.divider` rule.
* fix(web): move tx-type colour coding onto the accordion border
ColorCodedTxAccordion tinted its whole fill by tx type, so a transfer's
details panel was a mint-green block (`background.light`) rather than a card.
The fill is now `--card` and the level colour becomes a 1px border, so the
signal survives while the panel matches every other card. Uses the same colour
the method chip already shows - `success.main`, or `primary.main` in dark - so
border and chip read as one thing.
Drops the now-unused `background` entry from TxInfoColors.
* refactor(web): source SPACES_LIMIT from the spaces constants
Moves SPACES_LIMIT out of the Sidebar's config and into features/spaces/constants,
alongside the other spaces domain limits, and points SpacesList at it.
* fix(web): soften the tx-detail border, drop two stray borders and pad the sticky header
- The accordion outline used `success.main` (#00b460), which read as a strong
brand green rather than a border. Switches to the palette's `*-light` tier -
the weight we use for borders everywhere else, and what shadcn.css already
maps `--color-*-muted` to - so info, warning and success all outline in their
own light shade.
- Drops the accordion's hover fill. Greying the panel made the nested
"Advanced details" block blend into it; the border marks the region and the
trigger is its own affordance.
- Expanded queue items no longer take a `--border` outline. They sit on the
grey page, so the card edge is already legible.
- PageHeader's sticky bar had no bottom padding, leaving the tabs flush against
the scrolling content and reading as a cramped strip once pinned. Now
`--space-3`, taking it to 96px.
* fix(web): hold the queue row on one line far longer, then wrap deliberately
The row wrapped at 1350px into a flex fallback that packed every cell to the
left across three lines. Two things were forcing that early wrap:
- The column minimums were round numbers well above their content (170px for a
"-0.001 ETH" amount, 100px for an Execute button), totalling ~863px.
- `--grid-status` reserved a 72px minimum on every row, but queue rows only
render a status for pending/expired transactions, so that width was dead most
of the time.
Minimums are now the real rendered widths and status is `auto`, so the columns
come to ~510px and the single line survives to the 900px fallback - the same
breakpoint where the sidebar collapses. Cells get `min-width: 0` and the type
and date cells ellipsize, so the amount (the row's payload) has the largest
minimum and share of slack and never truncates; relative time truncates first.
Below 900px the queue row gets an explicit two-row grid instead of left-packed
flex - identity on top, timing and action beneath, nonce spanning both, action
against the right edge - and below 600px it unwinds to a stack with the action
row last. The dead 1000-1350px carve-out for history/bulk rows is replaced by a
600-900px one, since those have fewer columns and hold a line lower.
Verified one line at 1000px (654px row, amount intact), the two-row grid at
860px, and the mobile stack at 375px.
* fix(web): stop the topbar actions overflowing and tidy the mobile queue row
The topbar's action cluster was `shrink-0` with ~403px of chips, so on a 375px
screen it ran 36px past the header rather than reflowing — `shrink-0` was the
cause, since it stopped the card ever narrowing enough for the chips to wrap.
Dropping it and adding `flex-wrap min-w-0` lets them fall onto a second row
inside the card. Desktop is unaffected: at 1440px the cluster still measures
479px on one row, flush right, with all seven chips.
The phone template also put the confirmations badge in the nonce's column,
which sized that column to the 54px badge — indenting every row by ~70px and
stranding the badge alone in the left gutter. The nonce now has a column to
itself (30px) and the badge sits beside the Execute button.
Verified 0px document and header overflow at 375px.
* fix(web): strip the queue row to essentials on phones so it holds one line
Below 600px the row stacked five lines - direction label, note, amount, relative
time, then the action - which read as a jumble however it was ordered. It now
drops what is redundant or repeated in the expanded panel and keeps a single
32px line: nonce, direction arrow, amount, confirmations, action.
Hidden below 600px: the "Send" label (the arrow beside it already says so), the
note, the token logo and the relative time. The label and note go out of flow
rather than `display: none`, so screen readers still get them and their grid
tracks still collapse; the logo is decorative and the amount text names the
symbol anyway.
Two things this needed:
- TxSummary now composes TxTypeIcon + TxTypeText instead of the combined TxType
export, so this file owns the label's class. Reaching TxType's own module class
from here is not possible - CSS modules scope per file - and putting the rule in
TxType's stylesheet would have hidden the label in history rows too.
- The token logo needs `display: none !important`, because TokenIcon sets
`display: block` inline on the iframe.
Verified one 32px line at 375px (cells sum to exactly the 291px row, amount
intact, 0px document overflow) and 592px, with every element restored at 700px.
* fix(web): stop the queue amount ellipsizing on phones
The amount wanted 74px but only got 49px of the 291px row, so it rendered as
"-0.00…". My earlier check measured the grid cell rather than the `<b>` inside
it, which is why it read as untruncated.
Three reclaims cover the 25px shortfall:
- 4px column gap instead of 8px, across four gaps (16px)
- zeroing TokenAmount's own flex gap, dead space now that its logo is hidden (8px)
- letting the nonce size to "12" instead of the 30px floor the tablet rules set (6px)
The amount now gets exactly the 74px it asks for, and a 4-decimal value 84px,
both untruncated on one line with no document overflow at 375px.
* fix(web): drive the queue row off its container so it stops scrolling sideways
At a 920px viewport with the sidebar expanded the row has 638px, but the
one-line grid needs ~652px. The breakpoints were on the viewport, so the single
line stayed past the point it fitted and the trigger's `overflow-x-auto` turned
the 14px overflow into a horizontal scrollbar.
The accordion trigger is now a `@container` and the queue templates are
container queries, so the row sizes against the width it actually has and the
sidebar's state stops mattering. Thresholds are the row's own box: one line
above 680px, the two-row template below it, the stripped single line below 380px.
Also scopes the old viewport fallback to `:not(.queue)`. Left as-is it would have
handed queue rows the left-packing flex fallback whenever the viewport was under
900px but the row still had over 680px — a collapsed sidebar at 880px, say —
undoing the templates above.
Verified 0px trigger scroll and an untruncated amount at container widths 339,
638, 648, 818 and 828px (viewports 375, 700, 880, 920, 1100).
* fix(web): round the batched-transaction rows to the grey card's curve
The action rows are white on a `bg-muted` card with a 16px radius, but sat
square inside that curve: the rows' wrapper carried the 8px padding itself, so
its own border box started at the card's edge and there was nothing to clip
them.
Splitting that into a padding wrapper and a clipping wrapper gives the rows an
8px rounded clip - the card's 16px less the 8px inset - so the two are
concentric. Measured on the ExecuteBatch flow: 16px card, 8px inset, 8px clip.
Applied in both DecodedTxs and Multisend's compact branch, which render
byte-identical markup for this block.
* fix(web): use the default button size for the queue row's primary action
The queue row's Execute and Confirm buttons used `sm` (32px). They are the row's
primary action, so they now take the app's standard 36px control height.
Applied to both ExecuteTxButton and SignTxButton: they share the same row slot
via QueueActions, and changing one alone would leave Confirm and Execute rows at
different heights. `compact` is only used there, so nothing else moves.
Note this is height only - `sm` and `default` both use `px-4`, so the side
padding is unchanged. Widening needs either `action` (px-6, but its 40px height
fills a 40px row) or a new size.
* fix(web): stop the transfer actions menu overflowing the details panel
Restoring the To label in b2f236215 added 40px of label plus an 8px gap to the
row, but the address block kept its full content width — so the row overflowed
by exactly those 48px and pushed the trailing actions menu outside the panel,
over the audit log beside it.
Wrapping the address in `min-w-0 flex-1` lets it yield that width; it already
ellipsizes internally. Row overflow is now 0 and the menu sits 16px inside the
panel, where it was 32px outside.
* fix(web): give the topbar one consistent layout across widths
The two topbar groups rearranged themselves twice on the way down, so their
positions looked arbitrary:
- container 1636px: one row, selector left, actions right
- container 1149px: two rows, selector top-LEFT, actions bottom-RIGHT (diagonal)
- container 1041px: two rows, actions FIRST, selector second (order inverted)
Two causes. `order-1` flipped the groups at 1100px, moving the actions above the
selector. And `ml-auto` stayed in force between 1100px and the ~1250px where the
two actually stop fitting, so the actions hugged the right edge on their own row
while the selector sat at the left of the row above. `min-[900px]` also mixed a
viewport breakpoint into an otherwise container-driven rule.
Now there is one rule: side by side above a 1260px container, otherwise a
full-width row each — same order, same left edge, at every width. Verified at
containers 1041, 1134, 1336 and 1636px.
* fix(web): stop the topbar actions card stretching across its wrapped row
`WIDE_ACTIONS_WRAP` repeated the context slot's `basis-full`, but `flex-basis:
100%` sets a flex item's *width* — and unlike the invisible context slot the
actions are a painted card, so it spanned the whole row (951px measured at a
999px container) with its chips marooned at the left of ~650px of empty white.
The context slot's own `basis-full` already consumes the line and pushes the
actions below it, so the card only needs `ml-0` to drop the `ml-auto` that
right-aligns it while the two share a row. It now measures its content width
(315px) at every width.
Fixing that exposed two defects the stretched card had been hiding:
- `max-[899px]:justify-end` right-aligned the context inside its full-width slot
while the actions stayed left, so below md the two wrapped rows sat on a
diagonal (search bar ending at 776px, card starting at 8px) — the thing the
single-layout change was meant to end. Both rows now start on the page's left
padding at every width.
- The burger was a sibling of the `basis-full` context, so it could never share
that line: it was stranded on a row of its own and the header grew to three
rows (204px) on mobile. It belongs to the left cluster, so it moves inside the
context slot — back to two rows (160px).
The existing tests asserted only that the named constants were applied, which
held while all three of these were broken, so they now pin the shape of the
rules instead: no `basis-full` on the actions, no `justify-end` on the context,
and the burger inside the context slot.
Verified in the browser at 375 / 800 / 1100 / 1229 / 1600 / 1700px: one row
above a 1260px container, two below, same left edge throughout, and no
horizontal scroll at 375px.
* fix(web): round the workspace switcher to the design system's base radius
`.spaceSelector` hard-coded `border-radius: 8px`, overriding the base
menu-button's `rounded-md`. 8px is the scale for the sidebar's 36px nav items,
so on this 48px row it read as barely rounded next to the Home pill directly
above it, which sits at 16px.
Uses `--radius-lg` (16px, the theme's `--radius`) so it matches that pill and is
tokenised rather than a literal. The class is shared with the
back-to-workspace button, which the surrounding comments already treat as one
visual pair, so both round together.
* fix(web): keep the account card top-right when the topbar stacks
The account chrome is the header's fixed point, so it shouldn't move when the
two groups stop fitting on one row. It now takes the top row (`order-first`) and
holds the right edge, with the search dropping underneath it. Dropping the old
`ml-0` is what leaves `ml-auto` in force — that override was pulling the card to
the left edge and leaving a wide gap where it had been.
Also rounds the collapsed sidebar's workspace switcher, and fixes a dead token
behind it:
- The switcher was 8px, the scale for the 36px nav items below it. In the
collapsed rail it sits in the top chrome with the logo and the sidebar
trigger, which are both 12px, so it takes 12px too.
- The avatar fills that 32px square exactly, so it has to match or its corners
poke out past the button's curve. It read `var(--rounded-md, 8px)` — a token
that exists under no name, so it had always been silently running on the
fallback. `--radius-md` is the real one, and unlike `--radius-xs`/`--radius-sm`
it is actually emitted at :root, so `var()` can reach it from a CSS module.
Verified in the browser at 375 / 1073 / 1229 / 1700px: account on the top row
flush to the right padding at every stacked width, search on the left below it,
one row above a 1260px container, no horizontal scroll at 375px. Collapsed rail
measured at 12px across the logo, trigger, switcher and avatar.
* fix(web): restore the welcome toggle's track, unclip the safe chevron, align the contacts search
Three unrelated visual defects.
Welcome Accounts/Workspaces toggle had no visible track. It was painted all
along — `bg-muted` is #f5f5f5 and the page behind it is #f4f4f4, a single value
apart, so it simply could not be seen. 3ac63e6ae introduced that when it moved
the track off paper onto `bg-muted` to match the reference's "muted track". The
pre-migration switch actually uses #fafafa, which goes the other way and gives
page -> track -> white active pill as three distinct steps; light mode now uses
that literal and dark keeps `--muted` (#262626 on a #121312 page has no such
clash, and this leaves dark provably unchanged). Only the `segmented` look
moves: its one product consumer is AccountsNavigation. The `default` look keeps
`bg-muted` because it sits on the white SecurityHub drawer, where it reads fine.
The comment says why it is a literal so it does not get tidied back.
Safe selector chevron rendered outside its box. `flex-1` still floors a flex
item at its min-content width, and the trigger wrapper's display content plus
its `pl-6`/`pr-12` came to more than the selector's fixed 515px box, so the
wrapper overflowed 19px to the right. The trigger lays the chevron out with
`justify-end`, so the chevron rode that overflow past the box edge (glyph
598-614 against a box ending at 611) and its padding ended up under the
nested-safes button 11px away. `min-w-0` lets it shrink: chevron now 587-603,
and the overlay stops exactly on the neighbour's edge. Side effect: a long
address now ellipsizes at this width instead of overflowing, which is what the
truncating address renderer is for.
Workspace contacts search sat 8px right of the table card below it, on a third
left edge from the table's own text. The actions wrapper beside it renders empty
for anyone without admin rights, but an empty flex item still takes the row's
`gap-2`. It is now only rendered when it holds an action, so the search starts
on the card's edge. Also `mb-4`, because the Tabs root's own `gap-2` left just
8px between the search and the table it filters.
Verified in the browser: track 250 vs page 244 in light and 38 in dark, chevron
inside the box with the overlay clear of the nested-safes button, search and
card both at x=88 with 24px between them. type-check, lint 0 errors, 6969 unit
tests and 253 Storybook snapshots pass.
* fix(web): size the topbar wrap threshold to the context variant
The header used one threshold for both wide context variants, sized for the
widest. The global-search variant is ~335px narrower than the safe bar, so on
space routes it broke to a second row while ~600px of the header was still
empty — the search stranded below the account card for no reason.
Split into a threshold per variant, measured rather than guessed. The header's
px-6 is excluded because a container query on `inline-size` resolves against the
content box (verified: a 1268px content box sits on one row under a 1260px rule,
a 1218px one wraps). The actions card is not one width either — a Safe route
carries more chips than a space route:
space route search 320 + actions 315 = 635 -> break under 660
safe route bar 655 + actions 479 = 1134 -> break under 1260
So only the search variant actually moves; the safe bar keeps 1260, which its
1134px really does need.
Worth recording for whoever tunes these next: a threshold set BELOW what the
pair needs does not avoid the wrap, it just makes flexbox do it unprompted — and
then `order-first` never applies, so the context lands on top and the account
stops being the header's fixed element. Measured exactly that while trying 1000
for the safe route: at a 1003px content box it wrapped with the context on top.
Verified in the browser per variant — space route: one row at a 1268px content
box, stacks at 653 with the account on top and right-aligned; safe route: one row
at 1338 with zero overlap, stacks at 1003 with the account on top. No horizontal
scroll at any width tested. type-check, lint 0 errors, 6970 unit tests and 253
Storybook snapshots pass.
* fix(web): let the safe bar grow when it wraps, and unclip logo-only chain badges
Two defects at small widths on the wallet view.
The safe bar overlapped the account card above it. `h-14` on the context slot —
added so the search input's `h-full` has a definite parent — is a fixed height,
and the safe bar wraps internally at narrow widths (to 104px measured). The slot
is `items-center`, so it centred the 48px of overflow: half of it spilled
*upwards*, 24px into the actions card, putting the bar's top edge at y=64 against
an actions card ending at y=80.
The slot height is now per variant, like the wrap threshold already was. The
search variant keeps the fixed `h-14`, because `h-full` needs a definite parent
and a min-height would leave the input collapsed to its content. The safe bar
gets `min-h-14` instead: same 56px floor, free to grow. Both are pinned by tests
so the two don't get collapsed back into one value.
The chain logo was cut in half, and this one is not the topbar's fault.
`.onlyLogo` resets `.indicator`'s 70px min-width, but ties with it on specificity
— so the `max-width: 899.95px` block further down the file, which drops
`.indicator` to a 35px floor, silently won on source order and put a floor back.
Logo-only badges render inside a 24px round `overflow-hidden` wrapper, so a 35px
indicator hung out of the clip. Compounding the selector (`.indicator.onlyLogo`)
makes the reset win on specificity regardless of order. The `inline` variant is
unaffected — its `.onlyLogo` already won on source order — so the only behaviour
change is non-inline logo-only badges below 900px losing a floor that was wrong
for them: all 18 call sites are logo-only.
Verified in the browser at 620px: slot 104px, safe bar top flush with the slot
and clear of the actions card, indicator span 22px inside the 24px clip with the
image fully inside it. type-check, lint 0 errors, 6970 unit tests and 253
Storybook snapshots pass.
* fix(web): drop action/submit buttons to h-9 and even up the sidebar rail
Three things.
Action buttons are now 36px, not 40px: `action` and `submit` both move to `h-9`,
the same height as `default`. The wider px-6 and larger icon are what set an
action apart, not a taller box. `lg` (h-10) and `xl` (h-12) are untouched and
remain the deliberate exceptions, so anything that really wants 40px asks for
`lg` rather than overriding a height at the call site. This reaches ~48 direct
call sites plus the ActionButton preset — Send/Receive/Swap, Confirm/Cancel,
Execute, Export and the dialog footers.
Sidebar nav items were less rounded than the New transaction button above them.
The cause is the dead token again: `.sidebarInteractive` read
`var(--rounded-md, 8px)`, which exists under no name, so it had always used that
8px fallback and quietly overrode the base menu-button's own `rounded-md`.
`--radius-md` is the real token (12px) — the value the New transaction button was
already rendering at — so the rail now agrees with itself.
The collapsed New transaction button was bigger than everything beside it. Its
icon-mode rule set `w-9` but left the size's `h-10` standing, so it sat 36x40 in
a rail of 32px squares. `size-8!` fixes both axes, the `!` being what beats
`h-10` — the same trick SidebarMenuButton's own icon-mode sizing uses.
Verified in the browser. Collapsed rail is now uniform at 32x32 @ 12px across the
trigger, back-to-workspace, New transaction and every nav item (logo stays 40px);
expanded, the nav items and New transaction share the 12px radius; Send and
Receive measure 36px tall. type-check, lint 0 errors, 6970 unit tests and 253
Storybook snapshots pass — one ReviewTransactionV2 snapshot updated for the new
button height.
* fix(web): keep the collapsed sidebar rail at the default 36px control size
The icon rail shrank its buttons to 32px squares while every other control in the
app sits at the default 36px. They now stay 36px and stay square, so collapsing
the sidebar changes the width of the items and nothing else.
- SidebarMenuButton's icon-mode sizing goes from `size-8!` to `size-9!`. This is
the shared shadcn primitive, but the only consumers are the Safe sidebar's own
subtree, so the blast radius is that rail.
- The New transaction button follows with the same `size-9!`.
- The sidebar trigger was `icon-sm` (32px) in both states. It now asks for `icon`
(36px) at its call site rather than changing the shadcn default, since
collapsed it sits in the same column as the nav items and a 4px-smaller square
makes the rail read as ragged.
- The workspace switcher / back-to-workspace avatar filled the button only
because a rule pinned it to a fixed 32px, which happened to match while the
rail was 32px. It now fills its button, so it tracks the rail size instead of
leaving a 2px ring once the square grew. (My first attempt at this added a
second rule and lost to that pre-existing one on specificity — 0-2-0 against
0-4-0 — so this edits the original rather than layering over it.)
Verified in the browser, dark mode as it happens: collapsed, every item in the
rail is 36x36 @ 12px (the logo stays 40px) and the avatar measures 36x36 inside
its 36x36 button; expanded is untouched — nav items 36px tall, New transaction
40px, the back-to-workspace row 48px with its avatar still 32px inset beside the
name. type-check, lint 0 errors, 6970 unit tests and 253 Storybook snapshots pass.
* fix(web): move the address book header actions off lg onto action
The legacy address book's New entry / Import / Export were `size="lg"` (40px)
while the Spaces address book renders the same three actions at `size="action"`.
Its own twin settles which is right, so the legacy header follows: 36px, and the
row is internally consistent again.
Realigns the search fields that were sized to those buttons. Both address books
asked for `inputSize="lg"` specifically to match a 40px button, so dropping
`action`/`submit` to h-9 left the field 4px taller than the buttons beside it —
in the Spaces one too, where the buttons were already `action`. Both are now
`default` (h-9), and the pairing is documented where it is decided: InputGroup's
size comment still claimed `lg` pairs with `action`/`submit`.
Left alone deliberately: the Safe accounts header pairs an `lg` Add accounts
button with an `lg` search, and Safe apps pairs an `lg` search with an `lg`
Select — both internally consistent, so whether those rows should also come down
to 36px is a design call rather than a mismatch to fix. The new-safe create/load
wizard footers are likewise still `lg`.
type-check, lint 0 errors, 6970 unit tests and 253 Storybook snapshots pass;
measured in the browser — New entry, Import and Export all 36px.
* fix(web): left-align both topbar rows and keep the burger on the first one
Stacked, the account card held the right edge while the context sat left, and the
burger travelled down with the context instead of staying at the top-left.
Both fall out of reordering from the wrong side. Moving the actions with
`order-first` put them ahead of the burger, which is why the burger had to be
tucked inside the context slot in the first place — and then it followed the
context down a row. Moving the CONTEXT with `order-last` instead leaves the DOM
order (burger, context, actions) doing the right thing on its own: row 1 is
burger + actions, row 2 is the context. The burger is back to a plain header
child with no order class, so nothing can push it off the first row.
`ml-0` on the actions comes back alongside it, dropping the `ml-auto` that
right-aligns the card while the two share a row, so both wrapped rows now start
on the page's left padding.
One row above the threshold is untouched: context left, account flush to the
right padding, no overlap.
Verified in the browser at 620px — row 1 BURGER + ACCOUNT at x=8, row 2 context
at x=8 — and at 1600px, one row with the account right edge on the padding.
type-check, lint 0 errors, 6970 unit tests and 253 Storybook snapshots pass.
* fix(web): stop the share button opening two tooltips, and three smaller fixes
The transaction share button showed two tooltips on hover. TxShareLinkWrapper
already wraps its child in CopyTooltip, which supplies a TooltipTrigger and its
own "Copy the transaction URL" content — and TxSigners nested a second Tooltip
("Copy transaction link") inside it, so both opened. The inner one is gone; the
button's aria-label keeps the accessible name. Checked the other CopyTooltip
call sites, this was the only one that doubled up.
Accordion triggers get `cursor-pointer` on the shared primitive. Every trigger
opens its panel, so the affordance belongs there rather than being remembered per
call site — several were missing it, and a collapsed section gave no hint it
could be opened at all. `pointer-events-none` when disabled means the disabled
case needs no counterpart. Three snapshots updated, each a single added class.
The dashboard action row sat at the top of its column instead of level with the
balance. The comment claimed a stretched `justify-between` put it at the bottom,
but that only holds while the refresh hint above it is also rendered, and the
hint is conditional — alone in the column the actions went to the top. `mt-auto`
gets there either way.
type-check, lint 0 errors, 6970 unit tests and 253 Storybook snapshots pass.
* fix(web): restore the empty-batch illustration's missing shapes, match name field height
Three of the empty-batch illustration's six paths carry no fill of their own, and
the file's root is `fill="none"`. Under MUI's SvgIcon they inherited its
`fill: currentColor`; rendered bare through SVGR that attribute stands, so the
parcel, ring and sparkle around the arrow were invisible — only the circle and
the arrow (which do set `illustration-*-fill`) survived. The SVG itself is
byte-identical to dev, so this is the wrapper's doing, not the asset's:
`fill-current` on the render site puts back what SvgIcon supplied, and the three
classed paths keep their own colours.
Worth knowing for later: 28 SVGs under public/images pair a `fill="none"` root
with shapes that set neither a class nor a fill. Many are legitimately
stroke-only outlines, so this is not 28 bugs — but it is the shape of a migration
issue worth a deliberate sweep rather than fixing one illustration at a time.
Separately, the address book's Name field was h-9 above a 66px address box.
`Input` already has a `hero` tier at exactly 66px with the address wrapper's
radius, and SetAddressStep — the one other place pairing these two — already uses
it. EntryDialog and the two Spaces contact dialogs now do the same; measured 66px
against 66px.
type-check, lint 0 errors, 131 targeted tests and 253 Storybook snapshots pass.
* fix(web): polish the create-Safe flow layout
- CardStepper: `size="none"` so the progress track sits flush on the card's
top edge and the card's own overflow-hidden clips its square corners
- OwnerRow: align columns on their tops, not their bottoms — both are
label-above-control fields, and bottom-aligning pushed the address field
down by the height of the name field's caption. Read-only mode keeps
`self-end` since its address readout carries no label
- OverviewWidget: group the rows into one padded block with a single header
divider instead of a border-top per row
- InfoWidget: drop the leftover MUI overrides and the per-tip dividers; the
tips are one tinted block, not a divided list
- PayNowPayLater: put each radio's subtitle on its own line
* fix(web): let the tx-details dividers reach the card's edges
The details panel padded its own container, which held every separator — and
the vertical rule beside the audit log — short of the card's edges. Go
full-bleed on the panel and hand the inset to each block through
`--tx-details-edge-inset`, which each re-adds on whichever side meets an edge.
A border spans its element's border box, so padding moves the text without
moving the rule; text lands exactly where it did before. Covers the stacked
breakpoint, where the side that faced the vertical rule becomes an edge too.
* feat(web): add an info variant to Alert, use it for the macOS notice
The macOS notification helper was drawing an info-coloured border over the
page background with no icon. Give Alert a proper `info` variant — tinted
fill, no border, like `warning` — and let the notice use it with a Lucide
Info icon. The icon takes `info-strong` rather than `info-main` as the darker
of the two accents (2.04:1 vs 1.51:1 on the pale tint); neither is dark
enough for body copy, so that stays `text-foreground`.
Also drop the date filter's hand-rolled radius: Input's default `rounded-md`
already matches SelectTrigger, which is what keeps the field flush with the
selects beside it.
* refactor(web): replace hardcoded shield-line hex with a static token
Three shield logos inked their `.shield-lines` with a literal #121312. That
is exactly `--color-static-main`, which carries the same value in both
themes, so this is a no-op visually and leaves one fewer raw hex to drift.
* fix(web): restore the toast's edge margin, unbreak the tx-details rule
Toasts sat flush against the viewport's right edge. `.container` is pinned at
`right: 0` and MUI's Snackbar's own `margin: 1` was the only thing holding
them off it; replacing the Snackbar with a bare Alert dropped that margin.
The "Transaction details" rule stopped 32px short on the right because
`w-[calc(100%+32px)]` lost to Separator's base
`data-[orientation=horizontal]:w-full` on specificity (0-2-0 vs 0-1-0) — an
order the class list cannot change. Carrying the same variant lets
tailwind-merge drop `w-full` instead of leaving the two to fight.
* fix(web): keep swap rows on one line, make the app-card hover visible
A swap names two tokens where every other tx type names one, but the list's
`info` track has a 140px minimum sized for a single amount. `flex-wrap` let
the cell break onto a second line, so swap rows stood taller than their
neighbours and knocked the queue grid out of alignment. Nowrap plus min-w-0
hands the overflow to the children, whose own text-overflow truncates it —
the wrapper's `text-ellipsis` never applied, since text-overflow needs a
block box, not a flex container.
Safe App cards hovered from white to `--muted` (#f5f5f5), one value apart, so
the hover was invisible. Move it onto the border, which is also what it was
before the migration. Resting border is transparent, so picking up a colour
costs no reflow.
* chore: point the `web` dev-server config at the TanStack app
`preview_start({name: "web"})` now runs @safe-global/web-tanstack; the Next.js
app stays reachable as `web-next`. Dropped `autoPort` from `web` because
vite's config sets `strictPort: true` on 3000, so it could never have taken
effect — better to fail loudly than to look configurable.
* fix(web): stop tinting icons with the border token
Twelve icons took their colour from `text-border`. `--border` is #e5e5e5 in
light mode — about 1.2:1 against white, so those icons were all but invisible,
and next to a copy button on `--color-border-main` the pair read as two
different colours (the explorer icon beside every address, the address-book
and cloud badges, several info tooltips, the space context menu).
Point them at `--color-border-main` like every other muted icon, so the whole
set is one colour. `text-border` now has no icon usages left.
Snapshots: the four updated lines are all the same class swap, nothing else.
* fix(web): separate the card-table row hover from its buttons' hover
`.container tbody tr:hover` painted the row `--muted`, which is the very fill
the row's action buttons hover to — identical colours, so once the row lit up
the buttons gave no hover feedback at all. Use the mint `--color-background-light`
for the row so the buttons' grey reads on top of it. Picked over `--accent`
because it is declared at `:root` and so resolves outside `.shadcn-scope` too.
Affects the Address Book and the Spaces team/members tables.
Also centre the notification-centre unread count: as inline boxes the badge and
the heading shared a text baseline, leaving the 18px pill 4px below the centre
of a 20px/24px heading. Flex on both the row and the pill puts it on 0.
The two Storybook snapshots are the icon-token change from 3714abf4e, which I
pushed having run only the unit suite — these are the `.stories.test.tsx` pairs
I should have updated with it.
* fix(web): invert the card table's control hover instead of its row hover
Keeps the row's grey `--muted` hover and moves the contrast onto the controls:
their own `hover:bg-muted` was the same fill as the row, and a button is only
ever hovered while its row is, so the button hover was invisible. They now take
`--card`, the table's resting surface, so they read as lifting out of the lit
row rather than blending into it — and it stays darker-on-lighter in dark mode
(#171717 on #262626).
Replaces the mint row hover from cdb47f2cd, which solved the same collision from
the other side.
* chore(web): drop the shadcn migration spec docs
Fifteen planning and audit documents written while the migration was in
progress: the issue log, the component audit, the migration map/playbook/
contract, the design-system backlog/consistency/proposals notes, and the five
per-family specs. Their content has either landed in the code and its comments
or is stale, and they are not linked from anything.
The one inbound reference was a comment in the Argos Storybook workflow
pointing at an issue number in the log; dropped the pointer, kept the rationale.
* fix(web-tanstack): type beforePopState like Next's, not argument-less
The shim declared `beforePopState: (cb: () => boolean) => void`, but Next hands
its callback a `NextHistoryState`. Reused apps/web code reads it —
usePreventNavigation does `beforePopState(({ url }) => …)` to replay the
destination the user asked for — so that file type-checked clean against the real
router in apps/web and failed here with TS2345 + an implicit-any `url`.
Only the TanStack workspace's type-check sees this: apps/web resolves
`next/router` to Next itself, this workspace aliases it to the shim. It has been
red since the tx-flow AlertDialog change introduced the destructure; nothing
caught it because CI had not run on the branch since 2026-08-04.
Note the shim's `beforePopState` is still a no-op at runtime, so back/forward
blocking does not work in the TanStack app — a separate gap, not a type one.
* feat(web-tanstack): make beforePopState actually fire
The shim accepted a callback and dropped it, so the tx-flow's Back/Forward guard
did nothing in this app: a browser Back silently discarded an in-progress
transaction where the Next app asks first.
Registers the callback module-level and invokes it from a `popstate` listener with
the destination. Module-level rather than per-hook because Next keeps one callback
on its router singleton, replaces it on each call and never clears it — and
because `useRouter` hands out a fresh object every render.
`false` is not a veto here: a listener cannot cancel TanStack Router's sync to the
popped entry. It does not need to be — the browser has already moved the URL before
either router reacts, so blocking means rewinding either way, and the one consumer
(usePreventNavigation) replaces back to the previous path itself, using the return
value only to decide whether to. Kept in the type so call sites match across apps.
Two tests: the callback receives the popped destination, and a second registration
replaces the first instead of stacking.
* fix(web): resolve spending limit reset-time label before the select opens
* tests: update smoke test selectors for shadcn markup
* fix(web): restore choice button row layout and compact the tx header nonce
* tests: fix pk connect input and delete-tx flow for shadcn markup
* feat(web): sync alert warning with Figma and add outlined prop (#8468)
* feat(web): sync alert warning with Figma and add outlined prop
Warning follows the muted warning column (node 4316-12854): Figma
yellow tokens back warning-subtle/strong/accent in light mode, icons
take the brighter accent, and the border is gone on the filled style.
Destructive and warning gain an `outlined` prop (default true): the
card surface with the default border (nodes 1255-182975 / 4316-12819)
versus the borderless severity tint (nodes 1255-183763 / 4316-12854).
* docs(web): drop the Figma changelog block from the alert header
---------
* chore(web): add test ids to threshold select and transfer actions trigger
* tests: migrate add_owner and address_book regression selectors to shadcn markup
* tests: fix nft checkbox selectors and scope collectibles intercept to the API path
* tests: match the balances token by its testid, not loose text
`getByText(symbol)` is a case-insensitive substring match, so for ETH it resolved
to three elements — the amount ("0.1 ETH"), the token name ("Sepolia Ether", which
contains "Eth"), and the symbol itself — and failed on strict mode rather than on
anything being wrong with the page. Match `token-symbol`, which exists for this,
and assert the exact text.
Found while running the Playwright suite locally; no workflow currently runs it.
* tests: regenerate the swap-order snapshot for the new Alert classes
Left stale by the Figma alert sync (5631ffa84): rounded-lg→rounded-md, pr-18→pr-24,
the warning palette moving to warning-strong/warning-accent, and the description
slot gaining font-normal/break-words/min-w-0. Every changed line is an Alert class;
no other markup moved.
* fix(web): keep the queue bar's dismiss button in its header
The ✕ is positioned against the AccordionItem, which is the nearest `relative`
ancestor — and that grows to include the panel once the bar is expanded. So
`top-1/2` centred it in the whole open queue, dropping it ~150px below the header
into the transaction list. Correct while collapsed, wrong the moment it opened.
Pin it to half of TRANSACTION_BAR_HEIGHT instead, which is the header strip in
both states. Measured expanded: item 330px tall, ✕ centre and header centre both
666, fully inside the header box.
* fix: create safe (2) tests
* tests: fix multichain safe creation selectors for shadcn markup
* fix(web): keep the owner name input mounted while address resolution toggles the spinner
* tests: fix load safe owner and network selectors for shadcn markup
* fix(web): give the app-frame queue bar its surface back
The bar rendered fully transparent: MUI's <Accordion> was a Paper and brought a
background with it, so the CSS module never declared one; the shadcn Accordion is
an unstyled div, and the bar's own 50%-black backdrop showed straight through it
behind the rows.
Page grey rather than paper white — the queued transactions are white cards and
need the same surface behind them they get on the Transactions page, otherwise
they dissolve into the bar. Rounded at the top only (it is anchored to the bottom
edge) and lifted with the elevation Paper used to provide.
Also centre the header: the trigger's base is items-start, which top-aligned the
title and the chevron against this fixed 64px bar. Title, chevron and the dismiss
✕ now all sit on the header's centre line.
* fix(web): keep NameInput structure stable when adornments toggle
* fix: cy tests for tokens
* tests: handle hidden execution-method selector on unlimited-relay chains
* fix(web): keep the gas limit input mounted when the reset adornment toggles
* tests: fix address book delete dialog and send button selectors
* tests: commit rows-per-page selection via highlighted select item
* test: fix address book tests
* tests: fix batches tx
* test: fix create cf safe tests
* fix(web): toggle nested safe curation once per checkbox click
* fix(web): clear network multi-selector search text after selecting an option
* tests: stabilize safe creation and nested safe happy path flows
* tests: fix bulk-execution
* tests: safe_selector
* tests: click real curation checkboxes and tighten GTF execution-method assertions
* test: fix smoke tests
* fix: addMemberInput and spaces_basicflow tests
* test: fix TWAP integration tests
* fix(web): size the app preview drawer to its content and restore the close button label
* tests: adapt app cards, blind-signing discard prompt and token selects to shadcn markup
* tests: spending_limits
* tests: recovery, nested_safs_review
* tests: tx-history, tx_notes, tx_queue_reject_btn
* test: fix remove owner tests
* chore: update snapshots
* fix: topbar-header in 1 row instead of 2
* tests: verify threshold setup
* chore: update snapshots to make unit tests pass
* tests: create_safe_simple
* chore: temporarily disable the Agros Storybook workflow on every PR
* tests: e2e regressions
* test: tx_builder cleanup
* test: copilot
* tests: recovery
* test: snapshot fix
* fix: shadcn UI fixes (#8487)
* fix: Trusted modal UI
* fix: Staking Banner UI fixes
* fix: widget disclaimer IU
* fix: Manage Signers UI fixes
* fix: Add / Edit Member UI fixes
* fix: notification toasts UI
* fix: AB table UI fixes
* fix: multichain contact dialog UI fixes
* fix: Create Safe flow - UI fixes
* fix: centerred layout for Load New Safe flow
* fix: unit tests
* chore: update snapshots
* fix: bring back the existing styling for Trusted Safes modal
---------
Co-authored-by: Daniel Dimitrov <[email protected]>
Co-authored-by: Clóvis Neto <[email protected]>
Co-authored-by: vmosharova <[email protected]>
Co-authored-by: dsh <[email protected]>
Co-authored-by: Varya <[email protected]>
Co-authored-by: Hanzo Dev <[email protected]>