История коммитов

.
fix(auth): derive the login name of a nickname the same way everywhere
LoginName makes users.name_lat of a nickname and holds the rules a nickname is checked by; registration, external sign-in, the panel, the profile, the user search and the installer all use it. The installer only lowercased the nickname, so an administrator with a space or Cyrillic in it could not sign in.

AccountByLogin finds the account at the sign-in and at the password recovery by the nickname as typed, then by the login name, and renews an outdated login name on sign-in when it is free. A nickname of nothing but symbols is refused. The profile keeps the nickname as typed.
.
feat(admin): manage the bans of an account in the panel
The ban logic moves to the core: BanAccess, BanRepositoryInterface and BanManager (give, lift, delete, clear the history, with the karma penalty), named kinds BanKind and duration units BanDurationUnit. The profile of the site calls them instead of its own use cases, so its behaviour stays as its tests pin it.

The page of an account gets a Bans tab: the history, a ban given in a dialog with the kinds and lengths the visitor may give, lifting, deleting a record and clearing the history confirmed in dialogs. The Ban action of the list opens that dialog instead of the profile of the site.
.
feat(admin): edit an account, its password and its settings from the panel
/admin/users/{id}/edit changes the whole profile and the fields of the staff (nickname, gender, karma, notes); /admin/users/{id}/password sets a new password and may sign the account out everywhere; resetting the settings is confirmed in a dialog. Each needs the core permission and nobody reaches up the hierarchy (UserEditAccess).

The panel stores the text as typed; what the profile of the site stored escaped reads back as its text. A new nickname brings a new name_lat, so the account signs in under it.
.
feat(admin): give every account a page of its own in the panel
/admin/users/{id} shows the account without the profile module: roles and state, contacts and dates, what the user told about themselves, activity, the bans in force, where they came from and the notes of the staff, each part for whoever may see it. The list and the dialogs lead there; Open on the site appears only while the profile module is switched on.

The actions on an account move to UserActionsMapper, shared by the row of the list and the page. A hidden address in the list is shown only to whoever may read hidden addresses. New components: facts. New core enum Johncms\Users\BanKind names the kinds of a ban.
.
refactor(auth): declare the permissions on other accounts in the core
Opening an unconfirmed account, editing a profile, changing a password, resetting settings, reading notes and hidden addresses, and all the ban permissions move from ProfilePermissions to CorePermissions::USERS_*. The admin panel is about to use them, and the permissions of a module are purged from the roles when the module is removed. The keys are unchanged, so granted roles keep them; the Russian titles moved to the system domain with them.
.
fix(i18n): collect the strings inside the blocks of an embed
Twig keeps the body of an {% embed %} as a template of its own in the embedded_templates attribute of the module, outside the tree the scanner walked, so its strings never reached the .pot files.
.
feat(admin): confirm the removal of accounts in a dialog
Deleting an account and removing the accounts waiting for approval - one, all from an address, all of them - is confirmed in a dialog saying what would go, opened over the list. A link with data-admin-modal opens its address in the modal of the layout (modal.js); the controller answers the request of the script with the dialog alone and a plain visit with the same dialog as a page, so the profile and the panel without the script keep working.

Every action on the list returns to the page of the list it was taken from (back), checked against the list. New confirmation URLs: GET /admin/registrations/{id}/delete, /admin/registrations/delete-all, /admin/registrations/delete-by-ip.
.
feat(admin): rebuild the list of users and fold the staff and registrations into it
The list searches by nickname, e-mail, account number and IPv4, filters by role, activity and registration date, splits into all / awaiting approval / banned, sorts by its columns and counts above itself. It is a live list: live-list.js asks the same address again as the search is typed or a filter is picked and puts the regions back in place; without the script the links and the form work as plain pages.

Removed URLs: /admin/staff (now ?role=staff), /admin/registrations (now ?status=pending, the actions moved to the menu of the row), /admin/users/by-nick and /admin/users/by-ip (now ?sort=). The notification of new registrations leads to the list instead of the dead /admin/reg/. admin_counters() no longer returns users and staff.

New components of the panel: stat card, search field, filter dropdown, filter chips, sortable column head, user cell, row actions, empty state - all on /admin/style-guide.
.
feat(admin): move the panel into the shell of the new design
A sidebar with the sections of the menu, folding to its icons on a wide screen and sliding over the page on a narrow one; a top bar with the language, the color scheme, the notifications and the account; a page head with the breadcrumbs, the title and the page_actions and page_tabs blocks.

shell.js replaces menu.js; the styles of the old chrome are gone. An item a module adds to the menu of the panel names a Lucide icon.
.
feat(admin): restyle Bootstrap to the new design and add a style guide
The admin bundle paints the Bootstrap components from the tokens of the design (themes/default/src/admin/scss/ui/): one palette per color scheme, the green accent, the system font. The Bootstrap overrides of the old dark scheme are gone.

Icons of the panel come from Lucide: npm run icons builds icons/admin.svg from src/admin/icons/icons.json, @admin/components/ui/icon.twig prints one.

New URL: /admin/style-guide renders every component with its markup.