Skip to content

Fold the repeated renderer shells into one component each - #516

Open
Pixnop wants to merge 5 commits into
devfrom
refactor/489-renderer-shells
Open

Pixnop wants to merge 5 commits into
devfrom
refactor/489-renderer-shells

Conversation

@Pixnop

@Pixnop Pixnop commented Sep 20, 2026

Copy link
Copy Markdown
Contributor

Three of the five findings in #489 already landed on dev while the audit was being written, so this pass delivers the two that were left: the confirm dialog shell (item 2) and the rest of the select menu fold (item 3).

What changes

Item 2: one ConfirmDialog instead of eight copies of the same prompt (src/renderer/src/components/ui/ConfirmDialog.tsx, 65 lines).

Six inline prompts and two wrapper components each spelled out the same block: a question, a text-zinc-400 consequence line, and a ButtonsWrapper className="text-base" bgDark={false} equalWidth flush holding a secondary Cancel with PiXCircleDuotone and a confirm. All eight now call the shell:

Surface Was
ManageInstallationBackups.tsx restore, delete inline, 19 lines each
ListInstallations.tsx delete inline, 14 lines
ListVersions.tsx uninstall/unlink, restore vanilla, version in use inline, 13 to 20 lines each
ManageInstallationServers.tsx remove server inline, 9 lines
DeleteModDialog.tsx, RemoveServerModsDialog.tsx their own wrappers around the same shape

The shell owns no copy but the Cancel button. question, consequence, confirmLabel and confirmIcon are the caller's, because most of them read off what the player picked (ListVersions picks all four off versionToDelete?.linked). confirmVariant defaults to destructive and Restore Vanilla keeps its primary. Two content slots keep every prompt's DOM order exactly as it was: children between the question and the consequence (the mod name list, the truncated-scan note, the in-use warning box), beforeActions just above the buttons (the "delete data" checkbox).

ListVersions.tsx's "VS Version in use" prompt was not on the audit's list of six. It is a seventh copy of the same shape in a file whose other two just moved, and it needed no extra prop, so it went with them. The rename dialog in that file stays on PopupDialogPanel: it is a form, not a confirm.

Item 3: the last three hand-written select menus move onto SelectMenu.

LanguagesMenu.tsx (86 lines to 29), UIScale and ModDbCountPicker in ConfigPage.tsx (52 and 41 lines to one call each) each carried their own Listbox, AnimatePresence, motion.ul and option row. SelectMenu grew three optional props to take them: hint on an option for the muted note beside a label (a locale's translator, the default scale), listSize for the one panel long enough to need a scrollbar, and title for the ModDB row whose description doubles as its tooltip. ModDbCountPicker was a fifth copy the audit did not list; it is in the same file as UIScale and fits with no prop of its own beyond that tooltip.

Its trigger now renders from options.find instead of options.filter().map(). That idiom rebuilt the button node whenever the value changed, which takes the node out from under the focus Headless UI hands back after a pick, and rendered no trigger at all for a stored value outside the options.

git diff --shortstat origin/dev...HEAD: 10 files changed, 262 insertions, 332 deletions.

What stays

  • Item 1 (one button body behind NormalButton/FormButton/LinkButton/FormLinkButton) already landed in 8e4f6be9 on 15 September. Button and ButtonLink hold the body, the four presets set the defaults, the rest slot and FormButtonAction are intact.
  • Item 4 (TagsFilter and VersionsFilter onto one generic) already landed in 10179be1: components/ui/MultiSelectFilter.tsx exists and both filters are 33 and 32 lines.
  • Item 5 (the two Manage Mods dropdowns onto the shared constants) already landed in 4a104e59: InstalledModsSelectFilter.tsx and InstalledTagsFilter.tsx both import MENU_TRIGGER_STYLES and MENU_OPTION_STYLES.
  • InstalledModsSelectFilter does not become a preset over SelectMenu. Its trigger shows a placeholder in text-zinc-400 when nothing is picked and carries a title, and its first row is a clear option that is not one of the options. Folding it in costs three override props on the generic to save about twenty lines, which is the trade the audit warns against for InstalledTagsFilter. InstalledTagsFilter stays out for the reasons Fold the repeated renderer shells into one component each #489 gives.
  • LaunchBackupPrompt stays out, as the audit's own correction says.
  • The deliberate rules are untouched: Cancel first in the DOM in every confirm, aria-busy / aria-pressed / ariaExpanded, the focus-visible outline, the contrast floors tests/text-contrast.test.ts measures (it reads the shared constants and the files it names, none of which this pass moves), the hexagonal split, the path policy, the IPC validation and the mutation-tested guards.

Behaviour

No test was added, changed or deleted. The pinning tests pass as they stand: installationsDelete, installationsRestoreBackup, listVersions, manageInstallationServers, manageModsServerMods, manageMods, launchPlayGame (its buttons[0] is Cancel assertion), listModsFilterBar, modsListModsFilterUpdate, modsFilterLookupFailure, configPageModDbCount (which finds the ModDB trigger by its tooltip), selectableItemA11y, actionBusyState and text-contrast.

Callers checked by grep before each move: every PopupDialogPanel call in the four pages, every import of the two dialog components, and every SelectMenu caller. No exported function lost a caller; nothing was deleted that anything still calls.

Two changes a reviewer should see rather than take on trust:

  • The Manage Backups delete prompt used NormalButton where the other seven used FormButton. It now uses FormButton like the rest, which adds overflow-hidden to those two buttons and nothing else.
  • Picking an option in a SelectMenu no longer replaces the trigger node, so focus comes back to it. Verified by keyboard on the UI Scale row in the packaged build.

Gate, against dev's own figures:

dev this branch
typecheck pass pass
lint:ci 0 errors, 14 warnings 0 errors, 14 warnings, none new
format:check pass pass
Test files 242 passed 242 passed
Tests 4461 passed, 2 skipped 4461 passed, 2 skipped
Coverage (stmts / branch / funcs / lines) 94.57 / 91.01 / 95.22 / 96.26 94.57 / 91.01 / 95.22 / 96.26

git merge-tree against every open PR branch: no conflicts.

Headless check of the packaged build

npm run build:unpack, launched headless on a throwaway profile (two installations, two game versions, four mods, two backups, two server bookmarks), driven over CDP at 1280x720 and 1024x600. Every dialog and menu below was opened, read, closed with Escape and, where it has one, tabbed through. No renderer error in the log. Language switching still writes lang to localStorage and the UI scale still writes uiScale and data-uiscale.

1280x720 1024x600
Delete Installation (checkbox slot, Tab order: checkbox, Cancel, Delete)
Delete Backup (the one that changed button component)
Restore Backup (non-destructive icon, destructive confirm)
Uninstall VS Version
VS Version in use (warning box in the children slot)
Remove server (no consequence line)
Delete Mod
Language menu (scrolling panel, credits as hint)
UI Scale (all five rows, no scrollbar)
ModDB downloads (trigger keeps its tooltip)
Filter bar, Side and Installed axes

Closes #489. Part of #492.

Eight prompts spelled out the same shell: a question, a muted consequence
line, and a ButtonsWrapper holding a secondary Cancel and a confirm. Six were
inline, two were already components around the same shape.

ConfirmDialog holds it once. Cancel stays first in the DOM, so the focus trap
still lands on it. The Manage Backups delete prompt moves from NormalButton
to FormButton, which is what the other seven already used.
The language picker, the UI scale picker and the ModDB count picker each
spelled out the same Listbox scaffold: a trigger reading off the picked
option, an AnimatePresence, a motion list and a styled option row.

SelectMenu grows a hint for the muted note beside a label, a listSize for the
one list long enough to need a scrollbar, and a title for a row whose
description doubles as its tooltip. Its trigger now renders from a find
instead of a filtered map, so picking an option no longer replaces the button
node and a stored value outside the options still leaves a usable control.
It had one caller left once the picker moved onto SelectMenu.
This PR does not touch ActivityCenter.tsx or its test, but CI on ubuntu
still caught this test racing AnimatePresence's exit animation: the
region only leaves the DOM once the 200ms exit finishes, and under CI
load that outran even the 5s waitFor timeout (same root cause as #504,
already fixed the same way on the unrelated PR #509).

Popover.Panel's own Escape handler closes and returns focus
synchronously; assert on aria-expanded and focus, which is what Escape
actually controls, instead of waiting on the animation's timing.
Like the Escape case, this test is untouched by this PR but still caught
a CI load flake: useManageInstalledMods's rescan effect occasionally
fires an extra, idempotent time once a switch releases _updatingMods, on
top of the one it is expected to. Nothing about that changes what lands
on the folder or what the player sees, so pin the meaningful event
sequence (the scan the switch itself does, both writes, the renames) and
only require every event after it to be another harmless rescan, instead
of an exact count that races the same load-dependent timing.

Root-causing that extra rescan is tracked separately; it is not part of
this refactor's renderer shells.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant