diff --git a/assets/css/admin.css b/assets/css/admin.css
index 2bf4ce4..a5447bb 100644
--- a/assets/css/admin.css
+++ b/assets/css/admin.css
@@ -9,6 +9,11 @@
gap: 8px 16px;
}
+.lit-mcp-logo-link {
+ display: inline-block;
+ padding: 4px 0;
+}
+
.lit-mcp-logo {
display: block;
height: 30px;
@@ -25,6 +30,33 @@
margin-bottom: 1em;
}
+/*
+ * Phones: the tabs form an even grid instead of ragged wrapped lines, each at least 44 px high
+ * (core's tab is about 34 px). The active tab keeps core's look, marked by its colour and underline.
+ */
+@media screen and (max-width: 782px) {
+ .lit-mcp .nav-tab-wrapper {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(7.5rem, 1fr));
+ gap: 4px;
+ padding-bottom: 0;
+ border-bottom: 0;
+ }
+
+ .lit-mcp .nav-tab-wrapper .nav-tab {
+ float: none;
+ margin: 0;
+ padding: 12px 8px;
+ text-align: center;
+ border: 1px solid #c3c4c7;
+ }
+
+ .lit-mcp .nav-tab-wrapper .nav-tab-active {
+ border-color: #2271b1;
+ box-shadow: inset 0 -2px 0 #2271b1;
+ }
+}
+
/* Muted secondary text (tool names, client URLs). */
.lit-mcp-muted,
.lit-mcp-tool-name {
@@ -46,6 +78,19 @@
margin-bottom: 0.75em;
}
+/* Label and field share a line; the field takes the rest and wraps below on narrow screens. */
+.lit-mcp-url-line {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ gap: 6px 8px;
+}
+
+.lit-mcp-url-line .lit-mcp-copy-row {
+ flex: 1 1 22rem;
+ margin-top: 0;
+}
+
.lit-mcp-copy-row {
display: flex;
flex-wrap: wrap;
@@ -83,21 +128,59 @@
color: #50575e;
}
-.lit-mcp-status-yes-alt .dashicons,
-.lit-mcp-result-yes-alt .dashicons {
- color: #00a32a;
+.lit-mcp-state .dashicons {
+ vertical-align: text-bottom;
}
-.lit-mcp-status-warning .dashicons,
-.lit-mcp-result-warning .dashicons {
- color: #dba617;
+/* Divi tab: the report is a plain list, the icon replaces the bullet. */
+.lit-mcp-divi-report {
+ list-style: none;
+ margin-left: 0;
}
-.lit-mcp-status-dismiss .dashicons,
-.lit-mcp-result-dismiss .dashicons {
+.lit-mcp-divi-report ul {
+ list-style: disc;
+ margin-left: 2em;
+}
+
+.lit-mcp-status-yes-alt .dashicons {
+ color: #008a20;
+}
+
+.lit-mcp-status-warning .dashicons {
+ color: #996800;
+}
+
+.lit-mcp-status-dismiss .dashicons {
color: #d63638;
}
+/* Folded help ("Wo finde ich das?") and the Divi detail table. */
+.lit-mcp-hint,
+.lit-mcp-divi-details {
+ margin-top: 6px;
+}
+
+.lit-mcp-hint summary,
+.lit-mcp-divi-details summary {
+ cursor: pointer;
+}
+
+.lit-mcp-hint p {
+ max-width: 40rem;
+}
+
+.lit-mcp-divi-details summary {
+ font-size: 14px;
+ font-weight: 600;
+ margin: 1em 0 0.5em;
+}
+
+/* Only on phones: text that replaces a hidden column. */
+.lit-mcp-phone-only {
+ display: none;
+}
+
/* Tab "Zugriff". */
.lit-mcp-connections + .description {
margin-top: 8px;
@@ -113,6 +196,10 @@
display: none;
}
+ .lit-mcp-phone-only {
+ display: inline;
+ }
+
.lit-mcp-connections .column-actions {
width: auto;
}
@@ -122,7 +209,48 @@
}
}
-/* Tab "Tools": legend, collapsible groups (WordPress postboxes) with aligned columns. */
+/* Tab "Tools": search and open/close above the groups, legend, collapsible groups (WordPress postboxes). */
+.lit-mcp-toolbar {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ gap: 6px 12px;
+ margin: 0 0 12px;
+}
+
+.lit-mcp-tool-search {
+ min-width: 16rem;
+}
+
+.lit-mcp-filter-status {
+ color: #50575e;
+}
+
+.lit-mcp-groups [hidden] {
+ display: none !important;
+}
+
+.lit-mcp-group-reset {
+ font-size: 13px;
+}
+
+/* Phones: touch targets of at least 24 px (WCAG 2.5.8) for the reset link and the folded help. */
+@media screen and (max-width: 782px) {
+ .lit-mcp-group-reset {
+ min-height: 32px;
+ }
+
+ .lit-mcp-hint summary,
+ .lit-mcp-divi-details summary {
+ padding: 6px 0;
+ }
+}
+
+/* Explains the icons that replace the "Art" words on phones. */
+.lit-mcp-kind-legend {
+ display: none;
+}
+
.lit-mcp-legend {
display: grid;
grid-template-columns: max-content 1fr;
@@ -144,7 +272,13 @@
padding-left: 12px;
}
+/*
+ * The title keeps room of its own (12rem): when title, arrow and controls do not fit one line, the controls
+ * drop to the next line instead of squeezing the title into a narrow column or sending the arrow alone below it.
+ */
.lit-mcp-groups .postbox .hndle {
+ flex: 1 1 12rem;
+ min-width: 0;
cursor: pointer;
justify-content: flex-start;
gap: 6px;
@@ -254,12 +388,16 @@
}
@media screen and (max-width: 782px) {
- .lit-mcp-group-controls {
- order: 3;
+ /* No "order": title, toggle, controls read the same for the eye and for Tab. */
+ .lit-mcp-groups .lit-mcp-group-controls {
flex-basis: 100%;
padding-bottom: 10px;
}
+ .lit-mcp-kind-legend {
+ display: block;
+ }
+
/* Room for "Standard (indirekt)" in the select on phones. */
.lit-mcp-tools-table .column-tool {
width: 40%;
@@ -297,6 +435,45 @@
}
}
+/* Divi tab: on phones each row of the feature table becomes a card named by data-label. */
+@media screen and (max-width: 782px) {
+ .lit-mcp-divi-features thead {
+ position: absolute;
+ width: 1px;
+ height: 1px;
+ overflow: hidden;
+ clip-path: inset(50%);
+ }
+
+ .lit-mcp-divi-features tr,
+ .lit-mcp-divi-features td {
+ display: block;
+ }
+
+ /* One card per row: a rule and room between them, the feature name first and bold. */
+ .lit-mcp-divi-features tr {
+ padding: 8px 0;
+ border-bottom: 1px solid #c3c4c7;
+ }
+
+ .lit-mcp-divi-features td:first-child {
+ font-weight: 600;
+ }
+
+ .lit-mcp-divi-features code {
+ overflow-wrap: anywhere;
+ }
+
+ .lit-mcp-divi-features td::before {
+ content: attr(data-label) ": ";
+ font-weight: 600;
+ }
+
+ .lit-mcp-divi-features ul {
+ margin: 0;
+ }
+}
+
/* Tab "Protokoll": filters in their own block above the table (core hides ".tablenav .actions"
on phones); a core-style list table whose change is the wide primary column. */
.lit-mcp-log-filters {
@@ -311,6 +488,13 @@
max-width: 14rem;
}
+.lit-mcp-dates {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ gap: 6px 8px;
+}
+
.lit-mcp-date {
display: flex;
align-items: center;
diff --git a/assets/js/admin.js b/assets/js/admin.js
index 2eeb77e..0c8fd4a 100644
--- a/assets/js/admin.js
+++ b/assets/js/admin.js
@@ -1,5 +1,5 @@
-/* Einstellungen → LIT MCP: copy the server URL, live group feedback and collapsible tool
- groups, filter paging, and a question before a role save that locks people out. */
+/* Einstellungen → LIT MCP: copy the server URL, live group feedback, search and reset for the
+ collapsible tool groups, filter paging, and questions before a destructive or locking-out save. */
( function () {
'use strict';
@@ -119,6 +119,206 @@
}
}
+ function setGroupOpen( group, open ) {
+ var toggle = group.querySelector( '.handlediv' );
+
+ group.classList.toggle( 'closed', ! open );
+ if ( toggle ) {
+ toggle.setAttribute( 'aria-expanded', open ? 'true' : 'false' );
+ }
+ }
+
+ function saveGroupState() {
+ if ( window.postboxes && window.pagenow ) {
+ window.postboxes.save_state( window.pagenow );
+ }
+ }
+
+ function markAllUserSet( groups ) {
+ groups.forEach( function ( group ) {
+ group.dataset.litUserSet = '1';
+ } );
+ }
+
+ // Open or close every group; with `save`, remember it like a click on a single toggle would.
+ function setAllGroups( groups, open, save ) {
+ groups.forEach( function ( group ) {
+ setGroupOpen( group, open );
+ } );
+ if ( save ) {
+ saveGroupState();
+ }
+ }
+
+ // Tools: search by name, "only changed", open/close all. Rows and empty groups hide instead of vanish.
+ function setUpToolFilter() {
+ var groups = document.querySelector( '.lit-mcp-groups' );
+ var search = document.querySelector( '.lit-mcp-tool-search' );
+ var only = document.querySelector( '.lit-mcp-only-changed' );
+ var status = document.querySelector( '.lit-mcp-filter-status' );
+ var collapse = document.querySelector( '.lit-mcp-collapse-all' );
+ var expand = document.querySelector( '.lit-mcp-expand-all' );
+ var closedBefore = null;
+ var all;
+
+ if ( ! groups || ! search || ! only || ! collapse || ! expand ) {
+ return;
+ }
+ all = groups.querySelectorAll( '.lit-mcp-group' );
+
+ function countText( shown ) {
+ var text = window.litMcpAdmin;
+
+ if ( ! text ) {
+ return '';
+ }
+ return shown === 1 ? text.toolShown : text.toolsShown.replace( '%d', shown );
+ }
+
+ // The filter is cleared: forget the temporary state and, if the user changed groups meanwhile, save it now.
+ function finishFilter() {
+ var changed = false;
+
+ closedBefore = null;
+ all.forEach( function ( group ) {
+ changed = changed || !! group.dataset.litUserSet;
+ delete group.dataset.litUserSet;
+ } );
+ if ( changed ) {
+ saveGroupState();
+ }
+ }
+
+ function isFiltering() {
+ return search.value.trim() !== '' || only.checked;
+ }
+
+ function apply() {
+ var term = search.value.trim().toLowerCase();
+ var filtering = isFiltering();
+ var shown = 0;
+
+ /*
+ * A filter opens the groups with hits; clearing it puts every group back as the user had it.
+ * A group the user opens or closes themselves while filtering keeps that choice.
+ */
+ if ( filtering && closedBefore === null ) {
+ closedBefore = Array.prototype.map.call( all, function ( group ) {
+ return group.classList.contains( 'closed' );
+ } );
+ }
+ all.forEach( function ( group, index ) {
+ var visible = 0;
+
+ group.querySelectorAll( 'tbody tr' ).forEach( function ( row ) {
+ var select = row.querySelector( '.lit-mcp-tool-mode' );
+ var cell = row.querySelector( '.column-tool' );
+ var name = cell ? cell.textContent.toLowerCase() : '';
+ var match = ( term === '' || name.indexOf( term ) !== -1 ) &&
+ ( ! only.checked || ( select && select.value !== '' ) );
+
+ row.hidden = ! match;
+ visible += match ? 1 : 0;
+ } );
+ group.hidden = visible === 0;
+ if ( filtering && visible > 0 && ! group.dataset.litUserSet ) {
+ setGroupOpen( group, true );
+ } else if ( ! filtering && closedBefore && ! group.dataset.litUserSet ) {
+ setGroupOpen( group, ! closedBefore[ index ] );
+ }
+ shown += visible;
+ } );
+ if ( ! filtering && closedBefore ) {
+ finishFilter();
+ }
+ if ( status ) {
+ status.textContent = filtering ? countText( shown ) : '';
+ }
+ }
+
+ // Enter in the search field would submit the whole settings form, half-done changes included.
+ search.addEventListener( 'keydown', function ( event ) {
+ if ( event.key === 'Enter' ) {
+ event.preventDefault();
+ }
+ } );
+ search.addEventListener( 'input', apply );
+ only.addEventListener( 'change', apply );
+ // While filtering, nothing is saved: the hidden groups would be stored as hidden boxes.
+ // "User set" only means something while a filter is on; outside of it a stale flag would stop the next filter.
+ collapse.addEventListener( 'click', function () {
+ if ( isFiltering() ) {
+ markAllUserSet( all );
+ }
+ setAllGroups( all, false, ! isFiltering() );
+ } );
+ expand.addEventListener( 'click', function () {
+ if ( isFiltering() ) {
+ markAllUserSet( all );
+ }
+ setAllGroups( all, true, ! isFiltering() );
+ } );
+ groups.addEventListener( 'click', function ( event ) {
+ var group = event.target.closest( '.handlediv, .hndle' ) && event.target.closest( '.lit-mcp-group' );
+
+ if ( group && isFiltering() ) {
+ group.dataset.litUserSet = '1';
+ }
+ } );
+ // The browser may restore the search text after a reload; make the list match it.
+ apply();
+ }
+
+ // Tools: "Auf Standard zurücksetzen" clears a group's mode, its "nur lesen" switch and every tool's own setting.
+ function setUpGroupReset() {
+ document.querySelectorAll( '.lit-mcp-group' ).forEach( function ( group ) {
+ var button = group.querySelector( '.lit-mcp-group-reset' );
+
+ if ( ! button ) {
+ return;
+ }
+ button.addEventListener( 'click', function () {
+ var readonly = group.querySelector( '.lit-mcp-group-readonly' );
+ var mode = group.querySelector( '.lit-mcp-group-mode' );
+
+ // Order matters: unlock the rows first, then fall back to the default.
+ change( readonly, function () {
+ readonly.checked = false;
+ } );
+ change( mode, function () {
+ mode.value = '';
+ } );
+ group.querySelectorAll( '.lit-mcp-tool-mode' ).forEach( function ( select ) {
+ change( select, function () {
+ select.value = '';
+ } );
+ } );
+ if ( window.wp && window.wp.a11y && window.litMcpAdmin ) {
+ window.wp.a11y.speak( window.litMcpAdmin.groupReset );
+ }
+ } );
+ } );
+ }
+
+ function change( field, apply ) {
+ if ( ! field ) {
+ return;
+ }
+ apply();
+ field.dispatchEvent( new Event( 'change', { bubbles: true } ) );
+ }
+
+ // A button with data-lit-mcp-confirm asks first (revoke a connection, clear the log).
+ function setUpConfirmButtons() {
+ document.addEventListener( 'click', function ( event ) {
+ var button = event.target.closest( '[data-lit-mcp-confirm]' );
+
+ if ( button && ! window.confirm( button.dataset.litMcpConfirm ) ) {
+ event.preventDefault();
+ }
+ } );
+ }
+
// Protokoll: a changed filter starts again on page 1.
function setUpFilterPaging() {
document.querySelectorAll( '.lit-mcp-log-filters select, .lit-mcp-log-filters input' ).forEach(
@@ -162,9 +362,13 @@
}
document.addEventListener( 'DOMContentLoaded', function () {
+ // First: the confirmations guard destructive buttons and must not depend on the rest starting up.
+ setUpConfirmButtons();
setUpCopyButton();
setUpGroupToggles();
setUpLiveGroupFeedback();
+ setUpToolFilter();
+ setUpGroupReset();
setUpFilterPaging();
setUpAccessConfirmation();
} );
diff --git a/src/Admin/AccessTab.php b/src/Admin/AccessTab.php
index ead5a0b..72a399b 100644
--- a/src/Admin/AccessTab.php
+++ b/src/Admin/AccessTab.php
@@ -84,7 +84,7 @@ final class AccessTab implements AdminTab
private static function ownRoleWarning(): string
{
return __(
- 'Keine Ihrer Rollen ist mehr freigegeben – Sie selbst können LIT MCP nicht mehr nutzen.',
+ 'Keine deiner Rollen ist mehr freigegeben. Du kannst LIT MCP dann selbst nicht mehr nutzen.',
'wp-lit-mcp'
);
}
@@ -105,11 +105,14 @@ final class AccessTab implements AdminTab
);
foreach (wp_roles()->get_names() as $role => $name) {
printf(
- '
',
+ ''
+ . ' (%5$s)
',
esc_attr(Access::OPTION),
esc_attr((string) $role),
checked(in_array($role, $allowed, true), true, false),
- esc_html(translate_user_role((string) $name))
+ esc_html(translate_user_role((string) $name)),
+ esc_html(self::roleHint((string) $role))
);
}
echo '
' @@ -122,6 +125,19 @@ final class AccessTab implements AdminTab $this->renderConnections(); } + /** What a role may do in WordPress, in one phrase, so the admin sees whom they are letting in. */ + private static function roleHint(string $role): string + { + $caps = (array) (((array) wp_roles()->roles)[$role]['capabilities'] ?? []); + + return match (true) { + ! empty($caps['manage_options']) => __('verwaltet die Website', 'wp-lit-mcp'), + ! empty($caps['edit_others_posts']) => __('bearbeitet auch fremde Inhalte', 'wp-lit-mcp'), + ! empty($caps['edit_posts']) || ! empty($caps['edit_pages']) => __('bearbeitet Inhalte', 'wp-lit-mcp'), + default => __('keine Bearbeitungsrechte erkannt', 'wp-lit-mcp'), + }; + } + public function handleRevoke(): void { $this->action->guard(); @@ -240,17 +256,22 @@ final class AccessTab implements AdminTab echo '
%s
' + . esc_html__('Ein Assistent kann sich noch nicht verbinden.', 'wp-lit-mcp') . '
%s %s
', esc_html($title), esc_html($text)); + } + + $screen->add_help_tab([ + 'id' => 'lit-mcp-overview', + 'title' => __('Überblick', 'wp-lit-mcp'), + 'content' => $content, + ]); } public function registerSettings(): void @@ -87,6 +147,10 @@ final class SettingsPage 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'), + /* translators: %d: number of tools matching the filter */ + 'toolsShown' => __('%d Tools angezeigt', 'wp-lit-mcp'), + 'toolShown' => __('1 Tool angezeigt', 'wp-lit-mcp'), + 'groupReset' => __('Gruppe auf Standard zurückgesetzt. Noch nicht gespeichert.', 'wp-lit-mcp'), ]); } @@ -200,7 +264,7 @@ final class SettingsPage ], 'divi_withdrawn' => [ 'success', - __('Freigabe zurückgezogen; schreibende Divi-Werkzeuge sind wieder gesperrt.', 'wp-lit-mcp'), + __('Freigabe zurückgezogen; schreibende Divi-Tools sind wieder gesperrt.', 'wp-lit-mcp'), ], 'divi_withdraw_failed' => [ 'error', diff --git a/src/Admin/ToolsTab.php b/src/Admin/ToolsTab.php index 511b213..02ab265 100644 --- a/src/Admin/ToolsTab.php +++ b/src/Admin/ToolsTab.php @@ -87,6 +87,7 @@ final class ToolsTab implements AdminTab echo '