Repository navigation
fix(accounts): 🐛 polish the multi-account UI across items, switcher and sign-in - #59
Merged
Merged
Conversation
…oads useRemoteData keeps stale data only within the same account; a failed load now carries retry, and the event stream reports disconnected/incompatible first loads.
…atus - Switcher options carry a status dot and text, the SteamID tail (also in the trigger), and a wider list; fallback names keep their ending. - The server publishes which account the last QR sign-in was for, so signing an existing account in again closes the dialog and reports "Added…" or "Signed in to … again" with a shortcut to view it. Cancel is posted only while a sign-in is pending; the dialog reads "Sign in to <name> again". - Settings rows for broken accounts get Reconnect / Sign in again, Remove is an outline button, label saves show a pending state, and focus moves to Add account once a removed row disappears.
Pages remount per sidebar scope; stale filter values fall back to 'all'; selection is derived from visible rows; filtered-empty states offer Clear filters; the in-page account filter is now 'Filter by account' / 'Any account'; controls use a two-column grid on phones. The partial-connection notice becomes a banner naming the accounts with a fix action.
…apping Chips collapse into +N (named for screen readers), stickers without images are skipped, text is 11px with stronger contrast, narrow rows are taller. Highlights help is a clamped disclosure that explains each signal, and a light-theme-safe accent-strong token fixes the invisible gem.
Promise.allSettled across the accounts in view, skipping ones that aren't connected, so one failure no longer hides the rest; each failure is reported by account name.
… parts Shared Select instead of a native one, copy in config, text-success for profit, '—' when float is unknown, a skeleton while searching, and StatTrak filter values from one config list used by client and server.
Components that only need an account's name no longer import the zustand-backed scope store, so the marketing site's demo (which installs only site/ deps) bundles again.
… unnamed accounts
…when narrow DataTable measures its own width and drops optional columns by priority, so prices and floats never clip from the 960px Electron minimum up. Highlight chips follow the name column's width (container query) instead of the viewport. The footer row count uses the plural helper.
…e rows Phone rows drop the Highlights column: the tier and a reason count sit under the name and open a panel listing every reason, the pattern seed, exact float and attachments. The desktop Highlights cell opens the same panel. It is portalled out of the virtual rows, takes focus on open and returns it on Escape or Tab.
Failed revalidations keep the last good data but now report it, and a closed event stream is re-opened with backoff instead of going quiet. The banner says how old the screen is and Retry refetches without blanking it.
…opener, add skip link
…error screenshots
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes the multi-account UI issues from the two independent reviews: stale data and filters across account switches, overlapping item rows, the re-auth dialog that never closed, an ambiguous account switcher, and assorted a11y and convention gaps.
What changed
HIGH
+N(the hidden reasons are announced to screen readers). Stickers with no image are skipped instead of showing a broken glyph. Chip text is 11px with stronger contrast, and narrow table rows are taller.sm, the controls use a two-column grid with full-width search, and the toggles get their own row.useRemoteDatakeeps stale data only within the sameaccount=scope. On a scope change it returnsloadingunless the new path is cached.openinventory:organiser-rules:<steamId>). Legacy global rules are adopted only by the account whose units they all target. The run confirmation names the account.lastLogin(sequence, steamId, added), so re-signing an existing account closes the dialog. A banner then reports "Added …" or "Signed in to … again", with View … to switch to it. The dialog title reads "Sign in to again". Cancel is posted only while a sign-in is still pending, and the card-in-card layout is flattened.name (…0001). The trigger's status is read througharia-describedby.MEDIUM
accountTextuses the label copy instead of the raw key.Promise.allSettledover connected accounts only. They show a busy state, report each failure by account, and label their scope ("Covers: …").LoadError. Storage checkserrorbefore its empty states.--accent-strongtoken (#3f8f17 on light) fixes the invisible gem. The Highlights toggle and the help button are borderedButtons with a clear pressed state.Select, keeps its copy in config, colours profit withtext-success, shows—when the float is unknown, and shows a skeleton while searching. OneTRADE_UP_STATTRAK_FILTERlist serves both client and server.aria-labelledby/aria-describedby. A sharedAccountStatusDotalways pairs the dot with text.AccountStatususesrole=alerton the error text only. TiersHelp is a disclosure clamped to the viewport. The add button is 36px.Conventions:
AccountRowmoved into its own folder with.types.tsand.config.ts.SyncErrorBannerandTransferBannerprop types moved into.types.ts.account-gateandaccount-statusgained.config.ts. The items-page constants moved tolib/items/items-page.config.ts.authDotClassmoved intoaccount-status-dot.config.ts.Screenshots
All shots come from the mock API (3 accounts: 2 connected, 1 with a sign-in error; 6 items) at 1400×860 or 390×844.
+Nper cardAfter only
The re-auth flow (14, 15) and finder results (16) come from an extended copy of the mock that simulates a QR sign-in and returns sample contracts. The shared mock can't drive a login and has no finder results. Shot 17 is a dev server pointed at a dead backend.
—floatVerification
pnpm build(tsc -b + vite build): passes.pnpm lint: exits 0. The 4 warnings were all there before this PR (reveal refs, react-virtual incompatible-library ×2, triple-slash in steam.real).pnpm test: 191/191 pass.rebase --exec "tsc -b").site/(marketing demo):pnpm buildandpnpm typecheckpass with onlysite/dependencies installed. The first push broke the Vercel build, which the last two commits fix: the demo needed auseAppSnapshotstub, and the naming helpers moved out of the zustand-backed hooks.Caveats
lastLoginhas no server unit test. The real Steam QR flow, and focus return after a real account removal, were checked in code and against the mock only.+2; the full list is in the title and screen-reader text.Round 2
Fixes the 16 remaining issues from the re-review. Commits run from
4098b37to9d9d8a4, one per issue where possible.HIGH
minmax()floors (price 96px, float 64px, highlights 76px, item 208px, account 112px). DataTable measures its own width and hides optional columns by priority: Type first, then Location, then Account. Columns get a 12px gap, so right-aligned headers no longer run together. Highlight chips go by the name column's own width (a container query: 1 chip, then 2 from 220px, then 3 from 320px). The skeleton uses the same fit, including the Account column in the all-accounts view. At 960px with every account in view, about 24px is left spare for a classic Windows scrollbar.MEDIUM
accountDisplayName()returns the name and SteamID tail as separate parts. AnAccountNamecomponent truncates the name but never the tail. It is used in the Account column, grid cards, settings rows and the new phone subtitle line. Location options name their owner (bossman (…0001)) and keep the tail beside the location in the trigger. Sync, transfer and not-connected messages usename (…0001)too.MovePlan.blockedis new and covered by a test.LOW
aria-hidden.LoadingRegionstill announces "Loading…".#71717ato#6b6b74, so it clears 4.5:1 on hovered and selected rows too (axe flagged 4.39:1).h1(e.g. "Trade-up calculator") and explains per page why it needs one account. Trade-up and Cases say they are read-only.Account …0003, with no second tail. It fits the sidebar trigger whole.utils/plural.ts(pluralize,formatCount, with tests) gives "1 row", "1 item" and "1 item · Value" on storage units.EmptyState(icon, headline, reason).LoginCardtakesheadingLevel:h2in the dialog,h1on the first-run screen.account-actions,auth-gate,login-card,sign-in-screen,item-collectionandterminal-activatehave.config.tsfiles.item-collectionmaps each view mode to its renderer (satisfies Record<ItemViewMode, …>).auth-gatelists its first-load problem states. Transfer-bar's last inline labels also moved to config.Checks:
pnpm build✅ ·pnpm lint✅ (no new warnings) ·pnpm test194/194 ✅ ·site/pnpm typecheck+pnpm build✅ at9d9d8a4, also in a clean checkout with onlysite/dependencies installed. axe (WCAG 2.1 A/AA + best practice) reports 0 violations on: items loading skeleton, items for all accounts (dark and light), one account (light), phone light with the highlight panel open, phone dark, and the choose-account screen.Round 2 screenshots
All from the mock API (3 accounts: 2 connected, 1 errored; 7 items). Desktop shots are dark, phone shots are light.
Account …0003with one tail, fits the triggerRound 2 caveats
/itemsroute is the only one checked with axe at phone width. Other pages were checked at desktop width or not at all (Settings and Cases were not run).Round 3
Root cause behind the 768–1200px regressions: the item table chose its layout from the window width, but on Storage it sits in a pane beside the 288px units list. The table, its skeleton and the Storage split now size from their own width.
P1
DataTablemeasures its scroll area (useScrollArea, which replacesuseElementWidth). It hides optional columns, then switches to the compact item + price rows when the required columns no longer fit. Storage stacks the units strip above the contents until its own width reaches 56rem (a container query, about a 1170px window), so the contents table always gets room for its required columns. Item names also fit the row layout instead of the viewport: wide rows truncate to one line, compact rows wrap to two. Before, a wide table at 640px wrapped names and overflowed its 64px rows.mainis nowoverflow-y-auto, so content that can't shrink (the table's 288px floor on a short window) scrolls instead of being clipped.P2
lib/items/item-sort.config.ts).aria-busy; (b) the QR code (role="img", labelled) takes focus when it replaces Show QR code; (c) when a re-auth succeeds and its opener is gone, focus goes to the sign-in outcome banner; (d) when the stale banner resolves after Retry, focus goes to the pageh1; (e) the highlight panel follows its row while scrolling and returns focus to its button when it closes; (f) after a removal, focus moves to Add account once the POST has finished and the row is gone, in either order.main, so it never covers the bottom nav. The tier line no longer repeats the item name ("Tier 2 of 3 (Blue gem)").P3
h2under the pageh1.highlight-disclosure,account-row,login-outcome,tiers-help,stale-bannerandadd-account-dialog.event.target as Nodecasts are replaced withinstanceof Nodechecks.Also fixed (found by axe while testing): the dark theme's solid destructive button had white text on
#f87171(2.8:1). It now uses a--destructive-solidfill. Hovered primary buttons faded to 4.4:1 withopacity-90; solid buttons now darken on hover instead.Checks at
0381ba5:pnpm build✅ ·pnpm lint✅ (the same 4 warnings as before this round) ·pnpm test198/198 ✅ (new:data-table.utils.test.tsfor layout fitting and nulls-last sorting) ·site/pnpm typecheck+pnpm build✅ in a clean archive with onlysite/dependencies installed.axe (WCAG A/AA + best practice): 0 violations on Dashboard, Items, Storage, Organiser, Cases, Terminals, Trade-up and Settings at 768, 960, 1100 and 1440 (dark) and 390 (light). Also 0 on Items (all accounts) with the highlight panel open and the compact sort menu open at every width, and on the add-account, QR and remove-account dialogs in both themes.
Keyboard-only checks for items 9a–9f and 10 passed against the mock API, including forced failures, same-name accounts, a dropped event stream and add/remove account.
Round 3 screenshots
Mock API: 3 accounts (2 connected, 1 errored). Desktop shots are dark, phone shots are light.
Round 3 caveats
pointer: coarseor a width under 640px. It was checked in desktop Chromium at 390px, not on a real touch device.Round 4 UI QA
Checked at 390, 768, 960, 1100 and 1440px with a three-account mock. The Items table keeps its row heights when resized in both directions. Dashboard, Cases, Items, Storage, Settings and populated Trade-up pass Axe WCAG 2 A/AA and 2.1 A/AA checks in light and dark themes. The Trade-up candidate list now keeps names, prices, float and location visible on compact screens. App build, site build, lint and 198 tests pass.
These are mock API screenshots. They do not verify Steam sign-in, Electron, real transfers, or live marketplace prices.
Round 5 UI QA
An independent pass with an extended mock: 3 accounts (two named "bossman", one with a sign-in error), the site demo's 1,600+ items, terminals, cases, dashboard history, and trade-up candidates, odds and finder results. It covered 7 routes in all three scopes (All, …0001, …0002), at 390, 768, 960 and 1440 in both themes, plus live resize, a delayed account switch, keyboard and axe.
Fixed
scrollable-region-focusable(serious) at every width once finder results existed. Round 4's run had no results, so it missed this.Verified: 168 route × width × theme × scope combos with 0 axe violations, 0 page overflow and 0 console errors. With a 2.5s delay on the A→B switch, only skeletons show (no rows from A) and filters reset. Choosing the errored account offers Reconnect and Sign in again. App build, lint (0 errors, 4 existing warnings), 198/198 tests, and site build + typecheck all pass.
Still open (not changed): on phones, the Items and Storage tables scroll inside a short inner area. Short dropdowns (under 8 options) have no type-ahead. Finder results can't be loaded into the calculator. Terminals shows plain "Loading…" text instead of a skeleton.
Round 6 UI QA
The selected-account removal flow now returns keyboard focus to Add account after the dialog and account switch settle. Settings stays mounted while the selected account changes. The Items grid puts its selection and sort controls outside the ARIA grid; the populated grid passed Axe with zero violations at 390px and 1440px. Highlight chips now fit within the virtual row height, leaving a 12px gap between cards. The focused card's border and focus ring remain visible. Browser checks found no page overflow at those widths. App build, lint (zero errors, four existing warnings), and 198 tests pass.
Screenshots use a mock API. Independent provider scores for this head are pending.
Round 7 follow-up
The removal confirmation keeps its focused button available as an ARIA-disabled control while the request runs. A live status announces the working label; repeat submission is guarded. In a mock browser run covering eight keyboard/mouse, selected/nonselected, phone/desktop and response-order cases, focus stayed on “Removing…” during the request and ended on “Add account.” Terminals now says “Expires in less than 1 hour” for an active offer with under an hour remaining. App build, lint (zero errors), and 198 tests pass. The Round 6 screenshots above remain representative of the grid layout; independent scores for this head are pending.
Round 8 phone QA
The Items card grid has a 368px minimum frame, so a 272px card and its focus outline fit inside the scroll viewport even at 320×700 and 375×667. ArrowUp, PageUp and Home keep a 6px inset around the focused card. Trade-up finder outcome rows now shrink within the list at 320px; prices remain visible and the list no longer scrolls sideways. Browser checks at 320, 375, 390 and 1440px found no page overflow. The populated grid passed Axe at all four widths. App build, lint (zero errors), and 198 tests pass.
Screenshots use a populated mock. Independent scores for this head are pending.
Round 9 UI QA
Trade-up finder outcome names now truncate independently, so the probability and price stay visible at 320, 768, 1024 and 1100px. All 18 mock outcomes showed their odds at each width, with no list or page horizontal overflow. Disconnected-account pages now retain their route title above the account recovery card. Dashboard, Items, Storage, Cases, Terminals and Trade-up each had one
h1and zero Axe violations in the errored-account state at 390px. App build, lint (zero errors), and 198 tests pass.Screenshots use a populated mock. Independent scores for this head are pending.
Round 10 UI QA
Trade-up finder outcomes now choose their column count from the result list's own width. Full item names wrap; probabilities and prices remain visible. In the populated mock, none of 18 outcome names clipped at 320, 768, 1024, 1100 or 1440px; the list and page had no horizontal overflow, and Axe found no violations. Choosing an account from the single-account prompt moves keyboard focus to the destination page heading. Reconnect keeps its button focused while waiting and restores focus to the heading if the status card disappears; a delayed-reply mock confirmed focus through that transition. App build, lint (zero errors), and 198 tests pass.
Screenshots use a populated mock. Independent scores for this head are pending.
Round 11 UI QA
When a not-yet-connected account becomes ready, the account gate restores lost focus to the newly rendered page heading. A mock that delivered the live account update before the Reconnect response, and a separate choose-account flow, kept focus on the Trade-up heading after the page swap. At 320px, the mobile header gives the switcher room to show the account name and SteamID suffix, while compact Items rows use the thumbnail's width for readable names. The compact Price heading is a label because the sort menu already controls it. The Dashboard chart has enough left padding for a
$968.7Ky-axis label at 960px. Populated Items and Dashboard passed Axe with zero violations; app build, lint (zero errors) and 198 tests pass.Screenshots use a populated mock. Independent scores for this head are pending.
Round 12 UI QA
Trade-up calculator outcome names wrap fully on phones, with chance and price on a separate row. A browser check with realistic long StatTrak/wear names found no clipped outcome names or page overflow at 320, 360, 375, 390 and 414px. The compact Items sort trigger shows direction as “Price ↓” or “Price ↑” at 320 and 360px; the open menu keeps the full labels. Compact list thumbnails yield space to names through 375px. Dashboard chart labels fit all 47 supported currencies at 320 and 960px in a browser measurement. The repository
vp checkpasses with zero errors and four existing warnings; app build and 198 tests pass.Screenshots use a mock API. Independent scores for this head are pending.
Round 13 phone QA
Dashboard Top holdings uses a phone layout with the full item name, Qty, Each and Total on separate lines, while retaining its table from 640px upward. Browser checks at 320, 360, 390 and 414px in USD and IDR found no panel or page horizontal overflow, including a stress case with a long StatTrak name and a nine-digit IDR price. Compact Items rows give the name the thumbnail's width on phones, including 375px. Trade-up candidate rows wrap their float and storage location instead of truncating them at 320px. The loading skeleton follows the same phone holdings layout. Full
vp checkpasses with zero errors and four existing warnings; app build and 198 tests pass.Screenshots use a mock API. Independent scores for this head are pending.
Round 14: Cases rarity streaks and drop odds
The Cases view now shows cases since the latest exact-tier Mil-Spec, Restricted, Classified, Covert and Gold pull. A tier never pulled shows the full case count and “Never pulled”. The new chart compares observed case drop rates with the supplied reference odds (79.92%, 15.98%, 3.20%, 0.64%, 0.26%); unknown rarities are excluded from the rate denominator and called out. This comparison is shown for weapon cases only.
QA: app build, 199 tests, and
vp checkpass (0 errors, 4 existing warnings). Browser checks at 320, 390, 768, 960 and 1440px found no page overflow or Axe WCAG 2.1 AA violations in the populated mock Cases view.Round 15: Cases review fixes
QA: app build; site typecheck and build;
vp check(0 errors, 4 existing warnings); 320px account-choice and 390/1440px Cases browser checks with no page overflow or Axe violations. The populated monthly list and source link were checked by keyboard and DOM inspection.Round 16: Honest streaks and small-sample context
vp checkpass (0 errors, 4 existing warnings). Browser checks of populated and unknown-rarity cases at 390/1440px found no page overflow or Axe WCAG 2.1 AA violations.Round 4 review: focus, odds chart and accessibility
aria-disabled, presses ignored) instead of beingdisabled, and a polite live region announces “Syncing history…” when the request starts.useRestoreFocusOnRecoveryhook moves focus to the page heading once a load recovers from an error, and is applied to Items, Storage, Dashboard, Cases, Terminals and all three Trade-up loads. Focus the user has already moved elsewhere is left alone.role="group"named e.g. “Mil-Spec: your rate 80.000%, published odds 79.923% (8 of 10 pulls)”, and the bars arearia-hidden. A note says each tier has its own scale, and the small-sample caveat now explains the empty bar.<details>explainer in plain language appears when a streak is uncertain.case-rarity-insightsgets.config.tsand.types.ts(order, 500 threshold, decimals, classes, copy);sync-bannergets a.config.ts.QA: app build, 200 tests,
vp lint(0 errors, 4 existing warnings), site typecheck and build all pass. Axe (WCAG 2.1 AA) found no violations at 768, 960 and 1100px, in light and dark, on Dashboard, Items, Storage, Organiser, Settings, Cases, Trade-up and Terminals (48 checks), plus Cases at 390 and 1440px. With Playwright I tabbed and asserteddocument.activeElement: Sync stays on the button through the request; Retry lands on theh1; highlight Shift+Tab (panel and Close), Escape land on the trigger. Screenshots use a mock API.Round 5 review: consistency and wide layouts
aria-disabled(still focusable, presses ignored) and keeps focus after a failed retry. A successful retry still moves focus to the page heading.load-error,sync-bannerandpage-headerclasses moved into their.config.tsfiles.QA: app build, 204 tests,
vp check(0 errors, 4 existing warnings), site typecheck and build pass. Axe (all rules incl. best-practice, default tag set) found 0 violations on Dashboard, Items, Storage, Cases, Terminals, Trade-up and Settings at 768/960/1100/1440 dark and 390 light (35 checks), plus Items/Storage/Settings after a 1440→390→1440 resize round trip, and Cases in its error state. No page or main overflow at any width. Screenshots use a mock API.Round 6 review: compact rows, banner placement, strip cue and conventions
Location · floatline (plusPattern #nwhen there is one, folded into the same line) under the name, above the account line in All accounts. It truncates with the full text in a tooltip, so nothing overflows at 390 or 768. The compact "Price" header is now screen-reader-only because the sort menu already says it. Row heights went from 88/104 to 104/120. I checked the Redline (pattern plus highlight), Terminal (sealed and active, with Activate) and long-name rows in All accounts at 390 and 768 and none clip.#transfer-status), not above the heading, so theh1stays at the same y before and after a run. After "Move items" focus stays on "Run plan…" (it is nowaria-disabledwhile busy rather thandisabled, so it can take focus back). In the Items/Storage Move bar, Move hands focus to "Confirm move", Back returns it to Move, and a successful confirm moves focus to the status region before the bar unmounts.main). A rooterrorComponentshows a landmarked page with a heading and "Reload the app" instead of a blank screen. The organiser condition-value input is capped atmax-w-sm..config.tsfiles fortradeup-finder-results,plan-preview,organiser-panel,item-grid-card,bar-list,monthly-bars,rarity-picker,transfer-status, plus the Settings, Storage, Cases, Trade-up and Dashboard routes (settings-page,storage-page,cases,tradeup-page,dashboard-pageand a sharedpage-layoutconfig). No behaviour change there. The landing-site patch for the Trade-up slot label was dropped because the app now ships it.QA: app build, 206 tests (new coverage for the compact detail join),
vp check(0 errors, 4 existing warnings), site typecheck and build pass. Axe (WCAG 2.0/2.1/2.2 A and AA plus best-practice) found 0 violations on Dashboard, Items, Storage, Organiser, Cases, Trade-up, Terminals and Settings at 768/960/1100/1440 dark and 390 light (40 checks), plus Items, Storage and Settings through a 1440→390→1440→768→1100→960 resize sequence (15 checks); no horizontal page scroll in any of them. The keyboard checks for the banner and focus behaviour are Playwright scripts run against a mock API (they assertdocument.activeElement, they are not repo tests): Run plan… → Move items lands on "Run plan…" with theh1y unchanged; Move → Confirm move → Back → Move; Confirm lands on the status region; "+N more" pressed with Enter ends on a unit card; Tab then Enter on the skip link ends onmain.Root error page, 768px (an injected test throw, not a real failure):