feat: admin settings page UI/UX pass with WordPress-native patterns

- Header: h1 + logo in a wrapping flex row, hr.wp-header-end before the tabs,
  aria-current on the active tab; own CSS/JS enqueued only on this screen.
- Verbindung: ordered "So verbindest du Claude" steps, copy button (core
  clipboard.js + wp.a11y.speak), Site-Health-style status list, German details.
- Zugriff: roles as form-table fieldset, connections with site date format,
  empty-state row, confirm on revoke; ConnectionUsage records last use via
  WP_Application_Passwords::record_application_password_usage (once a day).
- Tools: plain-language legend, counter as inline notice, groups as core
  postboxes (postbox.js toggles, per-user closed state, summary when closed),
  fixed column widths with new "Art" column, submit above and below.
- Protokoll: site-timezone times with UTC tooltip, result icons, filters with
  labels in the list table nav, retention link to Limits.
- Limits: short labels with units and descriptions; snapshot count hidden but
  preserved.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-26 16:50:58 +02:00
co-authored by Claude Opus 5.5
parent 5965e32acb
commit 843f45aeec
22 changed files with 1535 additions and 242 deletions
+48 -12
View File
@@ -11,15 +11,24 @@ final class SettingsPage
{
public const SLUG = 'lit-mcp';
/** Hook suffix WordPress gives this page (add_options_page → "settings_page_<slug>"). */
public const SCREEN_HOOK = 'settings_page_' . self::SLUG;
/** @var array<string, AdminTab> */
private array $tabs = [];
/**
* @param list<AdminTab> $tabs The first tab is the default.
* @param string $logoUrl Loosen-IT logo shown top right of the page ('' = none).
* @param string $logoUrl Loosen-IT logo shown top right of the page ('' = none).
* @param string $assetsUrl URL of the plugin's assets/ directory ('' = no own CSS/JS).
* @param string $version Asset version for cache busting.
*/
public function __construct(array $tabs, private string $logoUrl = '')
{
public function __construct(
array $tabs,
private string $logoUrl = '',
private string $assetsUrl = '',
private string $version = ''
) {
foreach ($tabs as $tab) {
$this->tabs[$tab->slug()] = $tab;
}
@@ -29,6 +38,7 @@ final class SettingsPage
{
add_action('admin_menu', [$this, 'addMenu']);
add_action('admin_init', [$this, 'registerSettings']);
add_action('admin_enqueue_scripts', [$this, 'enqueueAssets']);
foreach ($this->tabs as $tab) {
$tab->registerActions();
}
@@ -52,6 +62,29 @@ final class SettingsPage
}
}
/**
* Own CSS and a small script, only on this page. The script builds on WordPress' own
* clipboard.js and wp.a11y.speak (as the media modal's "Copy URL") and postbox.js.
*/
public function enqueueAssets(string $hook): void
{
if ($hook !== self::SCREEN_HOOK || $this->assetsUrl === '') {
return;
}
$base = rtrim($this->assetsUrl, '/');
wp_enqueue_style('lit-mcp-admin', $base . '/css/admin.css', [], $this->version);
$deps = ['clipboard', 'wp-a11y'];
if ($this->currentTab() === 'tools') {
// Collapsible tool groups use WordPress' own postbox toggles (remembered per user).
$deps[] = 'postbox';
}
wp_enqueue_script('lit-mcp-admin', $base . '/js/admin.js', $deps, $this->version, true);
wp_localize_script('lit-mcp-admin', 'litMcpAdmin', [
'copied' => __('Die URL wurde in die Zwischenablage kopiert.', 'wp-lit-mcp'),
]);
}
/** @return list<string> */
public function tabSlugs(): array
{
@@ -81,16 +114,19 @@ final class SettingsPage
}
$current = $this->currentTab();
echo '<div class="wrap">';
echo '<div class="wrap lit-mcp">';
echo '<div class="lit-mcp-header"><h1>' . esc_html__('LIT MCP', 'wp-lit-mcp') . '</h1>';
$this->renderLogo();
echo '<h1>' . esc_html__('LIT MCP', 'wp-lit-mcp') . '</h1>';
// Admin notices of other plugins are moved right after this marker – above the tabs.
echo '</div><hr class="wp-header-end">';
$this->renderNotice();
echo '<nav class="nav-tab-wrapper">';
printf('<nav class="nav-tab-wrapper wp-clearfix" aria-label="%s">', esc_attr__('Bereiche', 'wp-lit-mcp'));
foreach ($this->tabs as $slug => $tab) {
printf(
'<a href="%s" class="nav-tab%s">%s</a>',
'<a href="%s" class="nav-tab%s"%s>%s</a>',
esc_url(self::url($slug)),
$slug === $current ? ' nav-tab-active' : '',
$slug === $current ? ' aria-current="page"' : '',
esc_html($tab->label())
);
}
@@ -106,11 +142,11 @@ final class SettingsPage
}
printf(
'<div class="lit-mcp-header"><a href="https://loosen-it.de" target="_blank" rel="noopener">'
. '<img src="%s" alt="Loosen-IT Logo" class="lit-mcp-logo"></a></div>'
. '<style>.lit-mcp-header{position:relative}.lit-mcp-logo{position:absolute;padding:10px;right:0;'
. 'top:0;height:30px;width:auto}@media (max-width:782px){.lit-mcp-logo{height:24px}}</style>',
esc_url($this->logoUrl)
'<a class="lit-mcp-logo-link" href="https://loosen-it.de" target="_blank" rel="noopener">'
. '<img src="%s" alt="Loosen-IT Logo" class="lit-mcp-logo">'
. '<span class="screen-reader-text"> %s</span></a>',
esc_url($this->logoUrl),
esc_html__('(öffnet in einem neuen Tab)', 'wp-lit-mcp')
);
}