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
+63 -1
View File
@@ -191,10 +191,72 @@ final class LogListTableTest extends TestCase
$table->single_row(['result' => 'ok']);
$ok = (string) ob_get_clean();
self::assertStringContainsString('<tr style=', $partial);
self::assertStringContainsString('<tr class="lit-mcp-partial">', $partial);
self::assertSame('<tr>', substr($ok, 0, 4));
}
public function testTimeColumnShowsSiteTimeInWordPressFormatWithUtcAsTitle(): void
{
Functions\when('get_option')->alias(static fn (string $key) => $key === 'date_format' ? 'd.m.Y' : 'H:i');
Functions\when('wp_date')->alias(static fn (string $format, int $ts): string => $format . '@' . $ts);
$table = new LogListTable(Mockery::mock(ChangeLog::class), ['ability' => '', 'user_id' => 0]);
self::assertSame('Zeit', $table->get_columns()['created_at']);
$html = $this->column($table, ['created_at' => '2026-09-26 10:39:13'], 'created_at');
self::assertStringContainsString('d.m.Y H:i@' . gmmktime(10, 39, 13, 9, 26, 2026), $html);
self::assertStringContainsString('title="UTC: 2026-09-26 10:39:13"', $html);
self::assertSame('–', $this->column($table, ['created_at' => 'kaputt'], 'created_at'));
}
/** @dataProvider results */
public function testResultColumnShowsIconAndWord(string $result, string $icon, string $word): void
{
$table = new LogListTable(Mockery::mock(ChangeLog::class), ['ability' => '', 'user_id' => 0]);
$html = $this->column($table, ['result' => $result], 'result');
self::assertStringContainsString('dashicons-' . $icon, $html);
self::assertStringContainsString($word, $html);
}
/** @return list<list<string>> */
public static function results(): array
{
return [
['ok', 'yes-alt', 'erfolgreich'],
['partial', 'warning', 'teilweise'],
['error<b>', 'dismiss', 'error&lt;b&gt;'],
];
}
public function testToolColumnIsMutedTextAndTopNavRendersTheGivenFilters(): void
{
$table = new LogListTable(Mockery::mock(ChangeLog::class), ['ability' => '', 'user_id' => 0], static function (): void {
echo '<select name="ability"></select>';
});
self::assertSame(
'<span class="lit-mcp-tool-name">lit/x&lt;</span>',
$this->column($table, ['ability' => 'lit/x<'], 'ability')
);
ob_start();
$table->display();
self::assertStringContainsString('<div class="alignleft actions"><select name="ability"></select></div>', (string) ob_get_clean());
}
/** @param array<string, mixed> $item */
private function column(LogListTable $table, array $item, string $column): string
{
$method = 'column_' . $column;
if (method_exists($table, $method)) {
return (string) (new \ReflectionMethod($table, $method))->invoke($table, $item);
}
return $this->columnDefault($table, $item, $column);
}
/** @param array<string, mixed> $item */
private function columnDefault(LogListTable $table, array $item, string $column): string
{