Skip to content

Moves the mods page onto the new page layout and toolbar (LAZ-947) - #23940

Open
r1chm8 wants to merge 9 commits into
masterfrom
laz-947
Open

Moves the mods page onto the new page layout and toolbar (LAZ-947)#23940
r1chm8 wants to merge 9 commits into
masterfrom
laz-947

Conversation

@r1chm8

@r1chm8 r1chm8 commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

https://linear.app/nexus-mods/issue/LAZ-947/implement-redesign-of-mods-page-layout-toolbar-and-table

Page & toolbar — The page renders

Page/PageHeader/PageScroll/PageContent and the new Toolbar instead of MainPage chrome and IconBar; the table gets the edge-to-edge sticky-header treatment the extensions page uses. A new hook adapts the existing mod-icons registrations into toolbar actions — hidden on a false condition, disabled on a string, ordered by registered position, as IconBar showed them. The five entries that were React components become plain actions (Install From File, Check for Updates, Deploy, Purge in the hook; Manage Rules in mod-dependency-manager) and their button components are deleted.

image

Order & icons

The row reads Install From File, Open, History, Check for Updates, Categories, Manage Rules. The "Open …" and "Import From …" actions each fold behind one button again, now via explicit groups rather than IconBar's icon-matching; a menu holding one action renders as that action. Manage Rules gets a rules icon of its own, Install From File a plus, and an unknown icon name shows a puzzle piece.

image

Menu/Button primitives

Menu rows take the same brand a Button does, so an action looks the same on the bar and in the overflow menu it collapses into; danger becomes a real Button brand; nxm-dropdown-item-active is renamed -focus (Headless UI 2 deprecated active, and -active misreads against CSS :active).

Bottom of the page

The multi-selection bar portals out of the table into the page's own bottom section via a new footerContainer prop (other tables keep the footer where it was), freeing the 48px the rows gave up to a placeholder. The two-selection hint becomes UsageAlert, keyed the same as Usage so existing dismissals carry. "Get more mods" stops sticking to the bottom, and the empty state becomes NoResults with a game-named button (or menu, where several sources offer one).

image

Table sizing & details pane

A sticky-header table's container takes flex-auto and spans the columns it overflows into. The details pane a double click opens now sticks to the top/bottom/right of the page scroll rather than standing the full height of the table and sliding out of view on horizontal scroll; PageScroll becomes a size container so it has something to measure.

image

Toolbar labels

The Open and Import buttons get their ... back, appended to the label so the words stay the translated key. Manage Rules now names its unresolved conflicts in the tooltip ("Manage Rules (3 unresolved conflicts)") instead of losing the flash it used to get: an action can carry a notice in its options, which the toolbar brackets after the title. mod-dependency-manager owns the text and now stores the count, not just a boolean.

image

Deliberate differences,

All revisited by the toolbar redesign: Deploy is highlighted by brand colour rather than the flashing class.

image

@r1chm8
r1chm8 requested review from a team as code owners August 13, 2026 13:16
@github-actions

Copy link
Copy Markdown

This PR has conflicts. You need to rebase the PR before it can be merged.

@github-actions

Copy link
Copy Markdown

This PR doesn't have conflicts anymore. It can be merged after all status checks have passed and it has been reviewed.

@r1chm8
r1chm8 force-pushed the laz-947 branch 2 times, most recently from 42b895d to 276239c Compare August 25, 2026 11:35
@github-actions

Copy link
Copy Markdown

This PR has conflicts. You need to rebase the PR before it can be merged.

r1chm8 added 9 commits August 25, 2026 15:26
…947)

The mods page now renders Page/PageHeader/PageScroll/PageContent and the new
Toolbar instead of the legacy MainPage chrome and IconBar, and its table gets
the edge-to-edge sticky-header treatment the extensions page uses.

The toolbar takes actions rather than components, so a new hook adapts the
mod-icons registrations for it — hidden when a condition returns false,
disabled when it returns a string, ordered by the registered position, exactly
as IconBar showed them. Anything that doesn't fit the width collapses into the
overflow menu, which ToolbarGroup already handles.

The five toolbar entries that were React components had no equivalent in the
action shape, so they are now plain actions with their behaviour carried over:
Install From File, Check for Updates, Deploy Mods and Purge Mods in the hook,
Manage Rules as a plain registration in mod-dependency-manager. The button
components they replace are deleted; CheckModVersionsButton stays because it
is still the table's row action.

Deliberate differences, all of which the toolbar redesign revisits:
- Deploy is highlighted via brand colour rather than the flashing class
- Manage Rules loses its unresolved-conflict emphasis
- a string condition greys an action out without showing the explanation

E2E selectors move from the ids those components carried to the data-testid
the toolbar renders.
A sticky-header table scrolls with the page rather than in a pane of its own,
so it carries `height: auto` and stopped at its last row. The details pane is
positioned against that container, which meant double-clicking a row in a
table holding one row opened a pane one row tall.

The container now also takes `flex-auto`, and the mods and extensions pages
lay their scroll content out as full-height flex columns so there is height to
grow into. `flex-auto` rather than `flex-1` keeps the basis on the content, so
a table taller than the page still scrolls it.

Both page-header toolbars take `flex-1 justify-end` for the same sort of
reason: as a flex item a toolbar is sized by its content, and a toolbar sized
by its content can't tell how much room it has, so it kept every action and
ran over the page title instead of collapsing into its overflow menu. Growing
rather than only shrinking is what lets it expand again when space returns.

The mods toolbar moves out of ModList into a component of its own.
The row reads Install From File, Open, History, Check for Updates, Categories,
Manage Rules, which moves Open to position 30 and History to 40. Position is a
single ordering for the row and the overflow menu both, so those two move up the
menu as well.

Open is new here: the five folder actions move out of `mod-icons` into a group of
their own, which the page folds into one button with a menu behind it, so they
take one slot on the row between them. `ToolbarPanelButton` now honours
`panelRole`, so that menu is styled as a dropdown and focuses its own first row
rather than being treated as a panel of settings — it hardcoded the latter.

Icons: Manage Rules asks for a "rules" icon of its own rather than borrowing the
profile transfer "connection" one, Install From File takes a plus, and an icon
name we have no path for now shows a puzzle piece — such a name comes from an
extension — rather than a generic shape.
…ton each (LAZ-947)

IconBar grouped every `mod-icons` action sharing an icon into one dropdown,
labelled with whatever leading words their titles had in common. That is how
the five "Open ..." actions came to sit behind an "Open" button, and the two
"Import From ..." ones behind an "Import" button, without anyone asking for it.
The new toolbar only knew about a hardcoded `mod-icons-open` group, so any
action that had relied on the icon grouping became a button of its own —
Witcher 3's "Open TW3 Documents Folder", and both importers.

An action now lands in one of those menus either by registering into the
menu's group, which is how to ask for it going forward, or by registering
into `mod-icons` under the menu's icon, which is what extensions we don't
ship still do. A menu holding a single action renders as that action rather
than a menu of one, the way IconBar did, so a game supporting only one
importer doesn't grow a button to reach it.
…LAZ-947)

A menu row now takes the same `brand` a Button does, so an action reads the
same on a toolbar as it does in the overflow menu it collapsed into — Deploy
Mods kept its primary colour on the bar and lost it the moment it collapsed.
`IToolbarAction` inherits the brand from `IMenuAction` rather than declaring
its own, so there is one union behind both. A row tints its icon and leaves
the labels an even column of text to read down; `danger` is the exception and
colours the whole row, a destructive action being the one thing in a menu that
should be hard to pick by accident.

`danger` was already a dropdown-item style but no Button brand answered to it,
so it becomes a real brand: the token ramp was there, only the `nxm-button-danger`
block and the union value were missing. `DropdownItem` takes a brand too, which
lets ToolRow's Delete row drop the class it applied by hand.

Also renames `nxm-dropdown-item-active` to `-focus`, along with the props and
state behind it. Headless UI 2 deprecated the `active` render prop in favour of
`focus`, and in a stylesheet `-active` reads against CSS's own `:active`, which
means something else entirely.
…pty state (LAZ-947)

Four things that all belonged to the bottom half of the page.

The table's multi-selection bar moves out of the table and into the page's own
bottom section, above the drop panel. `Table` takes a `footerContainer` and
portals the footer into it, so the bar lays out in the page's flow instead of
absolutely filling the bottom of the table — which also frees the 48px the rows
used to give up to a placeholder keeping them clear of it. A table that passes
no container keeps the footer where it was, which is every other table.

The drop panel gains a wrapper of its own to position against, since the bar is
now a sibling of it inside that section: the collapse toggle is absolute, and
with the `relative` still on the section it would have been positioning against
a box holding both. Collapsing reaches only the dropzone.

The hint standing in for the bar below two selections becomes an `Alert`. It is
still dismissed for good — `UsageAlert` keeps the decision in
`settings.interface.usage` under the same id `Usage` used, so anyone who has
already dismissed it won't see this one either. `Usage` stays for its four other
callers, which use the reopenable `persistent` mode this doesn't offer.

"Get more mods" stops sticking to the bottom of the page as you scroll and sits
after the last row, the page rather than the table being what scrolls now. Its
translucent band goes with the sticky, nothing passing behind it any more.

The empty state becomes `NoResults`, and its way out is a button named for the
game — "Get Skyrim SE mods" rather than "But don't worry, I know a place...",
which moves up to the message. Where more than one source offers somewhere to
browse it becomes a menu, on the design-system `Dropdown` rather than the
bootstrap one the bar still uses. The filter deciding that is now shared with
the bar rather than written out twice.

The details pane a row opens on a double click is positioned against the table
container, and a sticky-header table is as tall as its rows and as wide as its
columns — so the pane stood the full height of the table and slid out of view as
the page scrolled sideways. A layer between the two sticks to the top right of the
page scroll instead and stands as tall as it, which keeps the pane on the part of
the table you can see. The container spans the columns the table overflows into,
sticking only carrying as far as the containing block, and the scroll region
becomes a size container so the layer has something to measure. The pane's
collapse button takes the scrollbar's width as padding, or its icon would sit
behind a horizontal scrollbar.
The Open and Import buttons get the ellipsis IconBar's dropdown had, so a button
that leads to a choice of actions reads as one. It is appended rather than part of
the label, so the words stay the translated ones and the menu keeps them as its own
accessible name rather than announcing "Open dot dot dot".

Manage Rules says how many conflicts are waiting in its tooltip, where the button
used to flash for them. An action can now carry a `notice` in its options — what it
has to say beyond its title, which the toolbar puts in brackets after it and which
therefore reaches the tooltip, that being the label. It is read through the store so
it follows state: a conflict check dispatches after the change in mods that set it
going, so a notice read once at registration, or even at the render that change
caused, would be stale.

mod-dependency-manager owns the text, as it owns the state behind it. It now records
how many conflicts have neither a rule nor a resolution rather than only that some
have neither, so the tooltip can count them: "3 unresolved conflicts", or
"Unresolved conflicts" without a count. The boolean stays, derived from the count,
because uploadCollection reads it.
Giving the mods table's footer a place of its own meant `renderFooter` had to choose
between the portal and the placeholder, and it wrapped whatever the footer turned out to
be. But only the multi-row action bar needs the placeholder: that bar is absolutely
positioned so it fills a surrounding panel, and the placeholder is what holds the rows
clear of it. The usage hint shown below two selections lays out in the flow and takes its
own height, so a placeholder boxes it at 48px — on every table that passes no
`footerContainer`, which is all of them but the mods one.
The classic UI is still a choice the user has — Settings, Theme, "Use legacy Vortex UI" —
and this branch had left it with the redesigned page, because a page's `newLayout` was
decided without reference to which UI was running. Design asked for the old toolbar back.
An IconBar in the redesigned header read as neither one thing nor the other, so the
classic UI gets the whole page it had instead.

`newLayout` now also takes a callback, as `visible` already does, so a page that has both
renderings decides for itself which one it is about to draw. The mods page reads the
classic/modern setting; every other page keeps passing a plain `true` and is untouched.
`PagesContext` resolves it inside the memo that already watches that setting, so the
answer follows a change to it rather than being fixed when the page registered — the
toggle needs no restart — and `MainPageContainer` is handed a resolved boolean rather than
having to subscribe for itself.

ModList draws whichever of the two the layout asks for. The classic one is what it always
was: the toolbar in the chrome's header, the table in a panel with its footer inside it,
the drop zone below a FlexLayout. It needs the legacy `.main-page` wrappers, its header
being a portal into one of them — `MainPageHeader` renders nothing without that portal,
which is how the toolbar came to go missing in the first place. `ActivationButton`,
`DeactivationButton` and `InstallArchiveButton` come back for it, being what that bar drew
for Deploy, Purge and Install From File, along with `staticButtons` for the two the page
contributes itself rather than registering. `renderMoreModsLink` comes back for the empty
state, reusing the `browsableSources` filter the redesigned page already had rather than
repeating it.

Each bar draws only what it can. A component can't become an action — the row needs a
label and an icon it can measure, promote, collapse into the overflow and let the user
pin — so Deploy and Purge are registered as classic-only components and the new toolbar
skips them, building its own pair instead. Manage Rules is registered once for each: the
button that flashes while conflicts wait, and the action that says how many in its
tooltip. `isClassicOnly` and `isModernOnly` already existed for this and were already
honoured on the global-icons bars; they now mean something on a page toolbar too, which is
what lets one set of registrations feed two bars.

open-directory goes back to registering into `mod-icons` rather than a group of its own.
Its five "Open ..." actions still land behind the new toolbar's Open button, which adopts
them by their shared `open-ext` icon exactly as it does Witcher 3's — and the group it had
moved to has no classic consumer, so the old bar would have lost all five.

What the new toolbar cannot draw is now said at debug rather than warned: it fires on
every render, and a component registration reaching the classic bar is the arrangement
now rather than a surprise. The tutorials dropdown is the one that does — still gone from
the new toolbar, as the parent story asks, still on the old one.
@github-actions

Copy link
Copy Markdown

This PR doesn't have conflicts anymore. It can be merged after all status checks have passed and it has been reviewed.

@r1chm8

r1chm8 commented Aug 25, 2026

Copy link
Copy Markdown
Contributor Author

Latest commit shows the old page layout and toolbar when using the classic theme

image

@insomnious
insomnious requested review from insomnious and removed request for a team August 26, 2026 08:46
@github-actions

Copy link
Copy Markdown

This PR has conflicts. You need to rebase the PR before it can be merged.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant