fix(ui): one button language across condenser, the dialogs to match, and the v2.5.0 mobile polish

Twenty-seven commits in two halves. The first came out of testing v2.5.0 on a phone; the second out of a complaint that navigating the site turns up a different visual language on every screen. No tag was cut for the first, so they travel together.

One button, one language

The site's button — orange, 9px radius, Title Case — was defined inside .reveal. So it applied in dialogs, and to two pages that opted in by name, and nowhere else. Everywhere else Foundation answered instead, which is why the drafts list came out blue and each screen looked like it belonged to a different site. It was never a decision; it was an omission that nobody had reason to notice from inside any one screen.

Split into a look (the same everywhere) and a sizing (a dialog's action wants a comfortable minimum, a table row does not), with the look applied globally at root specificity, so a component that genuinely styles its own buttons still wins.

Doing that exposed what had been papering over it:

  • A disabled button was the theme's button colour at opacity 0.25 — a near-black ghost on a dark page, a smear on a light one. The editor carried a local override of exactly this; both are gone, replaced by one muted panel with secondary text.
  • Hollow buttons had two different borders depending on where they were, and a hollow delete drew Foundation's red-orange in light and a washed white in dark. One definition each now, in the site's red.
  • Three screens defined a button of their own and won on specificity: Settings carried a verbatim copy of the old global button (near-black, 18px, bold) plus its own copy of the old disabled treatment; the wallet's buttons were @extend .e-btn, the legacy themed button, the only ones in the app wearing it; and the sign-up screen painted its primary transparent, so the main action on the page read as a link.
  • Removing the Settings rule retired a .theme-dark .button.hollow override beneath it, which existed only to stop that rule's white fill leaking onto modal Cancel buttons — its own comment said so. Four hollow definitions became one.
  • Foundation's colour variants: .primary means what the site's primary means, so it takes the accent rather than #1a5099 — which is what had the community Subscribe button blue once its hardcoded black was removed. .secondary, .success and .warning no longer collect an orange halo they never asked for, and the five places still naming secondary in markup now say plain hollow, which is what they were already trying to look like.
  • The one hardcoded #1a5099 (login) and the lone dark-slate selected tab now use the accent, like every other active thing on the site.

The dialogs, and a rule that kept drifting

The confirmation dialogs are meant to share one shape: title, optional body, and an action row with the ways out first and the action being asked for last. That convention lived in a comment at the top of ConfirmDialog.scss and had drifted — the templates dialog shipped with Delete before Cancel, the editor's exit dialog put its primary between two secondaries, and two of the editor's modals used the shared action row with a shell of their own, restating the same centring and the same h4 margins. That last one is how you end up with one dialog centred and the next left-aligned.

All of them wear the shared shell now, and dialogConsistency.test reads the JSX and fails if an action row puts a secondary after the primary, declares two primaries, or sits outside a .ConfirmDialog. Both failure modes were mutation-checked against the real files.

Two dialogs that had built their own markup — the transaction-fee notice and the account-updated warning — moved onto it too, with a --prose modifier so their paragraphs and lists read left-aligned instead of centred.

A template you declined stayed applied

Not cosmetic, and the one worth reading twice. Loading a template in the advanced-settings dialog writes its beneficiaries straight into the store — it has to, or the summary under the editor goes on totalling the previous ones while the dialog shows the template's. But leaving without saving put nothing back, so a template you previewed and then declined stayed on your post.

Restored on unmount rather than on the Cancel button, so the dialog's × and Esc are covered too; the submit path marks itself saved and is left alone.

Three floating things and two corners

The assistant's button sits in a bottom corner the reader chooses. The pending-drafts card wanted one too, and so does the back-to-top button — which is how the card ended up sitting on top of the button, and later how the assistant ended up sitting on top of the card.

The rules are in one place and tested: the card takes whichever bottom corner the assistant has not claimed; while the back-to-top button is out it rises to the top of that same side, so the travel is vertical rather than across the screen, and doubles as a second, quieter nudge about the waiting draft; with the assistant holding both corners there is nowhere free, so the card stays up. The button needs no dodge of its own — the card leaves its corner exactly as the button arrives, both watching the same scroll depth, which now lives in app/utils/scrollDepth because two copies of 2 would eventually drift and the drift would look exactly like the collision it prevents.

My own regression, and how it was found

Pulling the modals' orange accent out to the border edge with left/right: -1px made the pseudo-element one pixel wider than the modal — and Foundation sets overflow-y: auto on .reveal, which makes the other axis compute to auto too. One pixel of overhang, a horizontal scrollbar across the bottom of every dialog in the app. Measured: 543px of content in a 542px box.

The accent is a background of the modal now rather than a bar laid on top of it. A background is clipped by border-radius for free, which also fixes what the -1px was reaching for: on the topmost scanline a rounded box is narrower than its own width, so a straight full-width bar sticks out at both ends, and its own radius cannot save it — a 3px-tall box has a 14px radius scaled down to something far flatter.

Smaller things:

  • The QR was generated at 240 and painted at 170, and qrious rounds the module size down and leaves the remainder as white — 8px on one side and 28 on the other, so the code neither filled its box nor sat centred, and how much it was off depended on the URL's length. The bitmap is read back, cropped to the code's own bounding box, and the quiet zone is CSS padding: even on four sides for any link.
  • The expiry dropdown in the AI model dialog drew two chevrons: it is a DropdownSelect now but kept the rule written for the native <select> it used to be. Its sibling below had already been migrated, with a comment saying exactly this.
  • That dropdown opened downward off the bottom of the panel and made it scroll. It measures against the nearest ancestor that actually clips its overflow now, rather than against .reveal (that dialog is not one) or the window.
  • The emoji picker rendered in emoji-mart's light theme on a dark page.
  • Settings put seven equal-width fields in one Foundation .row — a float grid, where a taller field drags the next one up beside it. That was the staircase of selects.
  • The activity log stranded its sixth filter tab on a line of its own inside a modal, and a community description arriving as one unbroken JSON blob pushed the modal sideways.
  • The voters list grew a scrollbar of its own (overflow-y: auto implies the other axis, and negative side margins made its content wider than its box), and its Close was the site's only ghost-painted dismiss action.
  • Community roles had no way back but the browser button, and the community name at the top is a link that nobody reads as one. g.go_back was already translated in all nine locales and unused.
  • Every button in the app carried margin-right: 15px !important from forms.scss, including the last one in a centred row, so no row was ever actually centred and no layout could opt out. Same declaration without the !important.

The first half: v2.5.0 on a phone

A back-to-top button

Nothing offered to take a reader back up a long thread but the scrollbar, and on a phone there isn't one. It appears after two screens of scrolling, so short views never carry it, and the scroll check is coalesced into a frame rather than run on every event.

The avatar, and why it took two attempts

Userpic needs the account's profile in the store and nothing fetched the signed-in user's own, so the avatar appeared only when some other view had already loaded it and was gone again on the next fresh load.

The first attempt took the session down on the dev site, and the reason is fixed first: fetchUserProfile threw when the bridge returned nothing, and an uncaught throw propagates out of a saga task, taking whatever else its parent was running with it. A profile that will not load is a missing avatar and a missing name, which is not worth a session. It fails quietly now, and its failure paths have tests — rejection, empty answer, no payload — because those are the ones that cost something.

$NaN beside a payout

The fiat figure is parseFloat(payout * pricePerBlurt).toFixed(2). Before the price feed arrives that product is NaN, and toFixed turns it into the string 'NaN' — neither undefined nor null, so the guard let it through and posts read 33.68 BLURT · $NaN. Fixed where all three callers meet.

Tables that had stopped looking like the site

.TableScroll, added in !181 (merged), restored a table's layout and width below 640px and left its corners square against a site whose tables are rounded to 12px. My own regression, and a plain one: I measured that the table filled its container and never looked at whether it still belonged.

Smaller things

  • /templates reported template: no posting key on a refresh or a bookmark. It says to sign in again now, and why.
  • CountryCode was a dead component with a native <select> and no callers — exactly what the next person needing a country picker would have copied.
  • NativeSelect never used a native select; it wraps DropdownSelect. It is OptionSelect now. Its stylesheet was an empty file with a live import.

Translations

149 keys were English in every other language: the AI-curated feed, search, notifications, settings, the terms dialog, the login warnings, the account-name validation messages, the voting notes, the menus. All translated into the eight locales. Scoped to keys the code references: en.json carries 841 strings and 573 are reachable, so the rest would be work nobody sees.

Eighty-odd existing translations had dropped their placeholder. es said 'Gente seguida por' — followed by nobody — where English says whose followers these are. Also 'Saldo' with no balance, 'desde' with no date, and a Korean reply count written %(count)개 instead of %(count)s개, which renders the raw text. None of it fails: the string appears, just without the thing it was meant to show, so it survives until someone reads that language.

Hence a test, not just a fix. It compares placeholders as a set: Korean legitimately repeats a name its grammar needs twice and counterpart substitutes every occurrence, so what it catches is a placeholder invented or lost.

Testing

49 suites, 433 tests, up from 44 and 388 on main. The new ones cover the back-to-top and drafts-card placement in every corner combination, the shared scroll depth and its two callers, dialog action-row ordering, the advanced settings dialog putting back what a declined template changed, the profile saga's failure paths, the payout guard and the placeholder parity.

The CSS was verified by compiling the stylesheet and rendering the real markup in headless Chrome in both themes — reading back computed colour, border and geometry, and looking at the result — rather than by reading the rules. Several of the bugs above are ones that only that catches.

Expecting more tweak for Firefox and Safari...

Edited by drakernoise

Merge request reports

Loading
Loading