✨ Rework settings as an index and one route per section - #231
Conversation
Settings was thirteen cards stacked on a single ~5000px route, every one of them the same weight, with a scroll-spy TOC held together by an IntersectionObserver, a bottom-of-page correction and a requestAnimationFrame that caught anchors the SPA had not rendered yet. Nothing could be searched, and the account's security state was scattered across three sections. The page becomes an index plus one route per section, with a rail on the left from lg up and a phone index that reads like a settings app: groups, current value under each name, tap through. - `nav.ts` holds the whole information architecture — five groups, fifteen sections, each with its icon, description, search keywords and the anchor it used to answer on. The rail, the phone index, the search, the section headers and the legacy-anchor redirects all read from it. - Search over the model, not the titles: every section and every control declares keywords, so "fuseau" (or "timezone") finds a row that lives inside Communications. ⌘K / Ctrl+K focuses it, ↓ walks into the results, Enter opens the first. - `SettingRow` replaces the `flex items-start justify-between gap-4` that nine sections had each rewritten, and carries the two things that kept drifting: one save indicator (saving → "Enregistré", fading on its own) and the error under its own control instead of at the foot of the card. - Running low on recovery codes is now an account-level banner on every settings screen plus a dot on the rail entry, not a warning icon in a card you have to scroll to. - Domain tiles show what hiding one would take out of the navigation, via a new `GET /library/domain-counts` that counts hidden domains too — the stats endpoints are scoped to enabled domains and cannot answer this. - The import screen opens on the last run (source, when, how many, or that it failed), via a new `GET /import/last-run`. - Connected devices lists this device first, then by last activity. - Deleting the account gets its own route instead of always being mounted at the end of the scroll; the four permanently disabled CSV exports collapse into one "bientôt" line; 2FA and privacy get skeletons instead of a jump. - `/app/settings#…` links already out in inboxes redirect to their new route, and `/app/settings/sessions` 308s to `/app/settings/appareils`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01STkSJw3WaXTPcyUFoMcaGk
|
The job dies inside GitHub's own Copilot Autofix agent before it looks at any code: Two things place it outside this change:
There's no fix to port: the 400 comes from GitHub's model routing, not from anything in the repo, so nothing in this diff (or any diff) changes it. It should clear on its own once that service accepts the model again. I can't re-run it either — the run is platform-owned and the API refuses the retry ( Generated by Claude Code |
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
knip flags it: the interface only ever types `SettingsSectionDef.entries` inside nav.ts, so the export was never reachable from anywhere else. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01STkSJw3WaXTPcyUFoMcaGk
Codecov flagged the patch at 0%: getDomainCounts and getLastRun shipped
without specs. Both are small mappers whose contract the settings UI leans
on — a domain absent rather than zero, and a run reported as `{ run: null }`
rather than an empty body — so pin exactly that.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01STkSJw3WaXTPcyUFoMcaGk
catalogs.spec.ts fails on them: "Compte", "Confidentialité", "Loomkeep" and "Résultats" already live in common.json, word for word in both locales, so the nav groups and the search heading read from there instead of restating them in other.json. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01STkSJw3WaXTPcyUFoMcaGk
Six things from testing the branch: - The current device's row coloured its own divider amber: `border-accent` sets every side, and Tailwind v4's `divide-y` puts a border-bottom on :not(:last-child). Only the left edge is accented now. - The legal footer floated mid-page, centred under nothing. It sits at the foot of the rail on desktop and at the end of the content on a phone. LegalLinks takes its alignment as an overridable default, since Tailwind resolves conflicting utilities by stylesheet order, not attribute order. - Settings had no way back to the profile on desktop, where the index (which carried the only back chevron) forwards to the first section. The rail opens with one. - The active rail entry's highlight is a single element that flies between entries, via a keyed crossfade, instead of blinking to its new place. - The global sidebar collapses to icons while in settings — two full sidebars for one page read as duplicated chrome. The stored pin is left alone, and hovering still opens it. - Search results take over the content column rather than replacing the nav: a list of rows you click through. A result that matched one control links to that control's anchor, and `flashAnchor` scrolls it into view and flashes it once on arrival — a short scale lift and an accent ring, both skipped under prefers-reduced-motion. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01STkSJw3WaXTPcyUFoMcaGk
Rows outside the settings search index render with no anchor, and comparing those directly against an empty fragment matched every one of them — a plain visit flashed the whole page. The decision moves into `isFlashTarget`, which is plain enough to test without a DOM, and the case that broke is now one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01STkSJw3WaXTPcyUFoMcaGk
…ge hit - The rail's highlight is now the same mechanism as the global sidebar's: one absolutely positioned pill measured off `aria-current="page"`, transitioning top and height. The crossfade it replaces paired two elements that never moved, so nothing travelled. Its accent edge was a `border-l` on a rounded box, which rendered as a crescent — the bar is its own square 2px element down the left edge now. - The legal footer left the 248px rail, where the inline "A · B · C" row wrapped mid-separator. One centred footer sits at the foot of the shell, which is a min-height flex column so a short section can't leave it floating mid-page. - Clicking a search result for the section you are already on changes only the fragment. Nothing remounts, so the mount-only flash silently did nothing in exactly the case search makes easy to reach. `flashAnchor` now takes the live hash and re-runs on it, and restarts its own animation when the same row is targeted twice. - The results view sets its own document title; the section it covers is still mounted in name only and kept the tab's. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01STkSJw3WaXTPcyUFoMcaGk
…n tiles Three more settings ideas from the review pass, plus two bugs found while verifying the previous push: - Security keys can now be renamed after creation (PATCH endpoint, no password confirmation needed — unlike add/remove, renaming changes nothing security-relevant), logged as MFA_WEBAUTHN_RENAMED. - Spoiler sensitivity (Contenu): AUTO/ALWAYS_HIDDEN/ALWAYS_REVEALED overrides the reviews/comments per-context "already finished this, so reveal" default rather than replacing it. - Domain tiles are drag-reorderable (Domaines), and the desktop rail's Library section now follows that order — a separate `domainOrder` preference, not a repurposing of `enabledDomains` (whose array order is deliberately kept canonical elsewhere). - Fix: a settings search result for "Buy Me a Coffee" pointed at support-buy-me-a-coffee, but the actual row id is support-bmc. - Fix: SettingsNav's indicator-persistence promise passed requestAnimationFrame's time argument to a `resolve()` expecting none. Migrations hand-authored and verified against a local Postgres instead of through `prisma migrate dev` — no dev DB reachable in this environment.
Lists what's actually bound today: ⌘K/Ctrl+K to search, Escape to clear it, and Alt+N to jump straight to each section — the list is generated from SETTINGS_SECTIONS so it can't drift out of sync as sections are added or removed. Linked from Aide & Feedback, searchable as "help-shortcuts". Adds a "keyboard" line icon (none of the existing ones fit; "key" is already the WebAuthn security-key icon, reusing it here would read as "security" in an unrelated context).
- Implemented a comprehensive account deletion section with a modal for password input and deletion confirmation. - Added detailed account deletion summary including deleted and anonymized categories. - Removed deprecated domains and double authentication routes, redirecting to new paths. - Revamped the domains settings page to include drag-and-drop functionality for domain management. - Enhanced the export settings page to allow JSON and CSV exports of user data. - Updated help and support sections with new external links and improved layout. - Removed obsolete routes related to data sources and suppression.
- communications/+page.svelte imported SettingRow from "./SettingRow.svelte" after the section got inlined out of components/ — the relative path needed to follow it to "../components/SettingRow.svelte". This alone was failing knip and, downstream, both web Docker builds. - navigation.spec.ts's "mobile shortcut choices" test passed isAdmin: false but still expected "admin" in the results — adminOnly correctly excludes it, so the test's own expectation was wrong. - navigation.spec.ts's domain-order tests scoped "Library section" by filtering the flat item list on `item.domain`, which also pulled in Tracking's Calendar entry (domain: MEDIA, gating only — it isn't part of Library). Scoped through visibleNavSections() to the one section where every item is domain-tagged instead.
…proved navigation
Three of the shortcuts page's new labels duplicated generic messages catalogs.spec.ts already guards against: search_page_title/common_search, viewer_previous/next vs common_image_previous/next. Point the page at the existing keys and drop the duplicates.
Summary *
Settings was thirteen identical cards stacked on one ~5000px route, navigated by a scroll-spy TOC held together by an
IntersectionObserver, a bottom-of-page correction and arequestAnimationFramethat caught anchors the SPA hadn't rendered yet. Nothing could be searched, and the account's security state was spread across three sections.It becomes an index plus one route per section: a rail on the left from
lgup, and a phone index that reads like a settings app — five groups, each section's current value under its name, tap through.nav.tsholds the whole information architecture (slug, icon, description, search keywords, the anchor it used to answer on) and the rail, the phone index, the search, the section headers and the legacy-anchor redirects all read from it.Search works over that model rather than over titles: every section and every control declares keywords, so "fuseau" — or "timezone" — finds a row that lives inside Communications. ⌘K / Ctrl K focuses it, ↓ walks into the results, Enter opens the first.
Alongside the navigation:
SettingRowreplaces theflex items-start justify-between gap-4nine sections had each rewritten, and carries the two things that kept drifting: one save indicator (saving → "Enregistré", fading on its own) and the error under its own control instead of at the foot of the card.GET /library/domain-countsthat counts hidden domains too — the stats endpoints are scoped toenabledDomainsand can't answer this.GET /import/last-run.Links already out in inboxes and push payloads keep working:
/app/settings#…redirects to the route that replaced the anchor, and/app/settings/sessions308s to/app/settings/appareils. The mail service and the in-app links now emit the new paths directly.Related *
Screenshots
Not captured — the app needs a running API and database, which this environment doesn't have. The layout is the "Sommaire & fiches" direction from the design artefact that preceded this change.
Checklist
search.spec.tscovers folding, the section-before-rows ordering, the cross-language keyword match and the social filter; the mail service spec follows the new link paths)frandenmessage catalogspnpm build:packagerun ifpackages/sharedchangedschema.prismachanged — n/a, no schema change (the two new endpoints read existing tables)--no-verify— hooks ran clean (pnpm check: 0 errors)🤖 Generated with Claude Code
https://claude.ai/code/session_01STkSJw3WaXTPcyUFoMcaGk
Generated by Claude Code