feat(ui): make every page hold up on a phone - #32
Merged
Conversation
There was a problem hiding this comment.
🔵 Needs a closer look
It touches many UI surfaces with largely untested responsive/layout changes, so it needs careful human verification across screens and devices.
Pull request overview
This PR makes the Nuxt UI frontend responsive at narrow/mobile widths by adding breakpoints, wrapping layouts, and consolidating repeated navigation/header controls (including a new shared back button and updated header/menu behavior), addressing the usability issues described in #31.
Changes:
- Restructures headers and action rows across many pages to wrap/collapse appropriately on small screens.
- Introduces shared UI components (notably
BackButton) and updates navigation controls (account menu, language dropdown). - Adds mobile Safari input font-size override and new i18n key for the language switcher aria-label.
File summaries
| File | Description |
|---|---|
| apps/app/i18n/locales/en.json | Adds nav.language translation for the language switcher aria-label. |
| apps/app/i18n/locales/fr.json | Adds nav.language translation for the language switcher aria-label. |
| apps/app/app/app.vue | Updates global header layout to hide attribution links below sm and keep auth controls accessible on mobile. |
| apps/app/app/assets/css/main.css | Adds (pointer: coarse) font-size override to prevent iOS Safari zoom-on-focus. |
| apps/app/app/components/AuthButton.vue | Converts account button into a dropdown menu and adds register/login CTAs for unauthenticated users. |
| apps/app/app/components/BackButton.vue | Adds a shared “back to create” button component with responsive label display. |
| apps/app/app/components/LanguageSwitcher.vue | Replaces USelect with UDropdownMenu so the trigger can collapse on phones. |
| apps/app/app/components/LegalTemplateDialog.vue | Adjusts dialog layout to prevent footer/actions and fields from overflowing on small screens. |
| apps/app/app/components/UnlimitedNumberField.vue | Makes number + “unlimited” controls wrap gracefully on narrow widths. |
| apps/app/app/pages/index.vue | Makes result/action rows and form grids wrap/stack appropriately on small screens. |
| apps/app/app/pages/dashboard.vue | Updates paste list rows to wrap badges and keep delete action visible on mobile. |
| apps/app/app/pages/login.vue | Adds BackButton to resolve navigation dead-end and fit header content on mobile. |
| apps/app/app/pages/register.vue | Adds BackButton to resolve navigation dead-end and fit header content on mobile. |
| apps/app/app/pages/p/[id].vue | Adds BackButton, improves long-secret wrapping, and constrains download button label on mobile. |
| apps/app/app/pages/account.vue | Stacks password+action rows on mobile and wraps long 2FA secrets/QR to avoid overflow. |
| apps/app/app/pages/legal/[slug].vue | Replaces bespoke back link with shared BackButton. |
| apps/app/app/pages/admin/users.vue | Wraps user row controls (role select + menu) for phone widths and improves long-text wrapping. |
| apps/app/app/pages/admin/storage.vue | Prevents quota/report labels from squeezing values off-screen by adjusting flex shrink behavior. |
| apps/app/app/pages/admin/settings.vue | Replaces bespoke back link with shared BackButton. |
| apps/app/app/pages/admin/legal.vue | Stacks tab controls on mobile and adjusts editor/preview heights for small screens. |
| apps/app/app/pages/admin/invitations.vue | Wraps invitation row content and keeps revoke action visible on mobile. |
| apps/app/app/pages/admin/banned-ips.vue | Wraps/line-breaks IP rows and keeps delete action visible on mobile. |
| apps/app/app/pages/admin/allowed-ips.vue | Wraps/line-breaks IP rows and keeps delete action visible on mobile. |
Review details
- Files reviewed: 23/23 changed files
- Comments generated: 1
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
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.
What this changes
Closes #31
A pass over every screen at a narrow width: the same markup, given the breakpoints it should have had. No new dependency, no layout system — labels that collapse to their icons below
sm, rows that wrap instead of pushing their action off screen, and two navigation gaps closed along the way.Why
Issue #31 has the full list. The short version is that the app was laid out at a desktop width and never revisited at 375 px, where the header alone needs about 430 px of the 343 px available.
Four decisions are worth reviewing rather than taking on trust:
The header drops the upstream links below
sm, not the sign-up button. GitHub and Documentation are attribution, and the comment on them says so; they now appear fromsmup. A phone header holds either those two or a labelled way to sign up, and signing up wins — it was previously reachable only from inside the login page, which hides it from exactly the people who need it. The button is bound toregistrationEnabled, the same condition the login page already used, so it is never a dead end on a closed instance.Fields are forced to 16 px on touch pointers, with
!important. Safari zooms the viewport whenever a focused field is under 16 px and every Nuxt UI control is 14 px, so tapping any field on an iPhone rescaled the page and left it there. The size comes from a utility class, so an element selector loses without it. Scoped to(pointer: coarse), which leaves every desktop untouched.The account button becomes a menu. It carries the account, the paste list and — for an
adminas well as asuper_admin, matching what/admin/settingsalready permits — the instance settings. That empties the shortcut rows pages were carrying in their own headers, which is what crowded a phone title most. The ten identical back buttons become oneBackButton, so the rule about when its label shows lives in one place./login,/registerand/p/:idwere dead ends and now carry that same button. On the paste page it is the first thing the screen asks of a reader who may never have used the instance; answering a secret with a secret is the obvious next step, so it reads as an offer rather than an interruption.Two smaller ones: the language
USelectbecomes aUDropdownMenuso its trigger can collapse like every other control, and long unbroken strings — a revealed secret, the TOTP secret — now wrap.Checks
pnpm lint,pnpm typecheckandpnpm testpasspnpm db:generate) — no schema changeNothing touches crypto or the read counter, and no server code changes at all. One authorization-adjacent line: the admin entry in the account menu is gated on
['admin', 'super_admin'].includes(user.role), the same test the dashboard shortcut it replaces already used. It only decides whether a link is drawn — every admin route still enforces its own session check server-side.Two i18n keys are added,
nav.languagein both locales, for the language menu'saria-label.AI assistance
See CONTRIBUTING.md. Disclosure is not held against you: it tells
the reviewer where to look hardest.
Nothing here is covered by a test: the 152 unit tests exercise pure logic and none of them renders a component, so lint and
vue-tscare the only automated checks this change has. It was verified by hand in the browser, screen by screen. Look hardest at the users screen, where the role select and the overflow menu now share a wrapping row, and at the account menu, which is the one place a link appears or disappears on a role.