Skip to content

✨ Rework settings as an index and one route per section - #231

Merged
Logan2234 merged 14 commits into
mainfrom
claude/settings-design-rework-4czmoh
Sep 17, 2026
Merged

Logan2234 merged 14 commits into
mainfrom
claude/settings-design-rework-4czmoh

Conversation

@Logan2234

Copy link
Copy Markdown
Owner

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 a requestAnimationFrame that 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 lg up, and a phone index that reads like a settings app — five groups, each section's current value under its name, tap through. nav.ts holds 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:

  • SettingRow replaces the flex items-start justify-between gap-4 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.
  • Recovery codes running low is now an account-level banner on every settings screen plus a dot on the rail entry, instead of a warning icon in a card you have to scroll to.
  • Domain tiles say 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 enabledDomains and can't answer this.
  • Import 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, with an accent rule on the current one.
  • 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 layout jump.

Links already out in inboxes and push payloads keep working: /app/settings#… redirects to the route that replaced the anchor, and /app/settings/sessions 308s to /app/settings/appareils. The mail service and the in-app links now emit the new paths directly.

Related *

  • n/a

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

  • Tests added/updated for the behavior this changes (search.spec.ts covers folding, the section-before-rows ordering, the cross-language keyword match and the social filter; the mail service spec follows the new link paths)
  • New user-facing strings added to both fr and en message catalogs
  • pnpm build:package run if packages/shared changed
  • A Prisma migration is included if schema.prisma changed — n/a, no schema change (the two new endpoints read existing tables)
  • No --no-verify — hooks ran clean (pnpm check: 0 errors)
  • Manual testing performed — not possible here, see Screenshots

🤖 Generated with Claude Code

https://claude.ai/code/session_01STkSJw3WaXTPcyUFoMcaGk


Generated by Claude Code

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
@github-actions github-actions Bot added api apps/api (NestJS) web apps/web (SvelteKit) shared packages/shared labels Sep 17, 2026

Logan2234 commented Sep 17, 2026

Copy link
Copy Markdown
Owner Author

github-advanced-security is red, and it isn't this PR's.

The job dies inside GitHub's own Copilot Autofix agent before it looks at any code:

Dt [SessionModelError]: Execution failed: CAPIError: 400 The requested model is not supported.
  COPILOT_AGENT_MODEL: sweagent-capi:claude-opus-5

Two things place it outside this change:

  • The check isn't defined in this repository — there's no workflow file behind it. It's a platform check injected by the Advanced Security / Copilot Autofix app, running against api.individual.githubcopilot.com.
  • It failed identically on ✨ Redesign reviews: slider modal, community summary, spoilers, reporting #230, which merged into main an hour ago with every other check green. So main carries the same red check right now.

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 (403 This workflow run cannot be retried). Every other check is still running; I'll keep watching and act on anything that comes back red.


Generated by Claude Code

claude and others added 13 commits September 17, 2026 12:01
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.
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.
@Logan2234
Logan2234 enabled auto-merge (squash) September 17, 2026 21:28
@Logan2234
Logan2234 disabled auto-merge September 17, 2026 21:28
@Logan2234
Logan2234 merged commit 8a92dc5 into main Sep 17, 2026
8 of 9 checks passed
@Logan2234
Logan2234 deleted the claude/settings-design-rework-4czmoh branch September 17, 2026 21:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

api apps/api (NestJS) shared packages/shared web apps/web (SvelteKit)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants