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
+51 -28
View File
@@ -94,21 +94,34 @@ final class LogTab implements AdminTab
$filters = $this->filters();
$extra = $this->extraFilters();
$days = $this->limits->logRetentionDays();
printf(
'<p>%s</p>',
esc_html(sprintf(
/* translators: %d: number of days */
__('Einträge werden nach %d Tagen automatisch gelöscht (einstellbar im Tab „Limits“).', 'wp-lit-mcp'),
$this->limits->logRetentionDays()
))
'<p class="description">%s %s</p>',
esc_html__('Das Protokoll enthält nur Verweise, keine Inhalte.', 'wp-lit-mcp'),
sprintf(
/* translators: %s: link "N Tagen" to the Limits tab */
esc_html__('Einträge werden nach %s automatisch gelöscht.', 'wp-lit-mcp'),
sprintf(
'<a href="%s">%s</a>',
esc_url(SettingsPage::url('limits')),
esc_html(sprintf(
/* translators: %d: number of days */
_n('%d Tag', '%d Tagen', $days, 'wp-lit-mcp'),
$days
))
)
)
);
printf('<p>%s</p>', esc_html__('Das Protokoll enthält nur Verweise, keine Inhalte.', 'wp-lit-mcp'));
$this->renderFilterForm($filters, $extra);
$table = new LogListTable($this->log, $filters + $extra);
echo '<form method="get">';
printf('<input type="hidden" name="page" value="%s">', esc_attr(SettingsPage::SLUG));
printf('<input type="hidden" name="tab" value="%s">', esc_attr($this->slug()));
$table = new LogListTable($this->log, $filters + $extra, function () use ($filters, $extra): void {
$this->renderFilterControls($filters, $extra);
});
$table->prepare_items();
$table->display();
echo '</form>';
$this->renderClearForm();
}
@@ -117,13 +130,11 @@ final class LogTab implements AdminTab
* @param array{ability: string, user_id: int} $filters
* @param array{result: string, date_from: string, date_to: string} $extra
*/
private function renderFilterForm(array $filters, array $extra): void
private function renderFilterControls(array $filters, array $extra): void
{
echo '<form method="get">';
printf('<input type="hidden" name="page" value="%s">', esc_attr(SettingsPage::SLUG));
printf('<input type="hidden" name="tab" value="%s">', esc_attr($this->slug()));
echo '<select name="ability"><option value="">' . esc_html__('Alle Tools', 'wp-lit-mcp') . '</option>';
self::filterLabel('ability', __('Nach Tool filtern', 'wp-lit-mcp'));
echo '<select name="ability" id="lit-mcp-filter-ability"><option value="">'
. esc_html__('Alle Tools', 'wp-lit-mcp') . '</option>';
foreach ($this->log->abilities() as $ability) {
printf(
'<option value="%1$s"%2$s>%1$s</option>',
@@ -131,39 +142,51 @@ final class LogTab implements AdminTab
selected($filters['ability'], $ability, false)
);
}
echo '</select> ';
echo '</select>';
self::filterLabel('user-id', __('Nach Benutzer filtern', 'wp-lit-mcp'));
wp_dropdown_users([
'name' => 'user_id',
'id' => 'lit-mcp-filter-user-id',
'show_option_all' => __('Alle Benutzer', 'wp-lit-mcp'),
'selected' => $filters['user_id'],
]);
echo ' ';
echo '<select name="result">';
self::filterLabel('result', __('Nach Ergebnis filtern', 'wp-lit-mcp'));
echo '<select name="result" id="lit-mcp-filter-result">';
printf('<option value="">%s</option>', esc_html__('Alle Ergebnisse', 'wp-lit-mcp'));
$labels = ['ok' => __('erfolgreich', 'wp-lit-mcp'), 'partial' => __('teilweise', 'wp-lit-mcp')];
foreach (self::RESULTS as $result) {
printf(
'<option value="%1$s"%2$s>%1$s</option>',
'<option value="%s"%s>%s</option>',
esc_attr($result),
selected($extra['result'], $result, false)
selected($extra['result'], $result, false),
esc_html($labels[$result])
);
}
echo '</select> ';
echo '</select>';
self::filterLabel('date-from', __('Von Datum', 'wp-lit-mcp'));
printf(
'<label>%s <input type="date" name="date_from" value="%s"></label> ',
esc_html__('von', 'wp-lit-mcp'),
'<input type="date" name="date_from" id="lit-mcp-filter-date-from" value="%s">',
esc_attr($extra['date_from'])
);
self::filterLabel('date-to', __('Bis Datum', 'wp-lit-mcp'));
printf(
'<label>%s <input type="date" name="date_to" value="%s"></label> ',
esc_html__('bis', 'wp-lit-mcp'),
'<input type="date" name="date_to" id="lit-mcp-filter-date-to" value="%s">',
esc_attr($extra['date_to'])
);
submit_button(__('Filtern', 'wp-lit-mcp'), 'secondary', '', false);
echo '</form>';
submit_button(__('Filtern', 'wp-lit-mcp'), '', 'filter_action', false, ['id' => 'lit-mcp-filter-submit']);
}
private static function filterLabel(string $field, string $text): void
{
printf(
'<label class="screen-reader-text" for="lit-mcp-filter-%s">%s</label>',
esc_attr($field),
esc_html($text)
);
}
private function renderClearForm(): void