diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml
index 9c76e9a..c90c300 100644
--- a/.github/workflows/quality.yml
+++ b/.github/workflows/quality.yml
@@ -90,6 +90,23 @@ jobs:
test -s "$zip"
unzip -t "$zip"
for required in \
+ functionalities/includes/admin/class-admin-icons.php \
+ functionalities/assets/brand/functionalities.svg \
+ functionalities/assets/brand/functionalities-admin.svg \
+ functionalities/assets/css/admin-brand.css \
+ functionalities/assets/css/admin-icons.css \
+ functionalities/assets/css/admin-polish.css \
+ functionalities/assets/js/admin-polish.js \
+ functionalities/assets/js/admin-link-health.js \
+ functionalities/assets/icons/settings.svg \
+ functionalities/assets/icons/arrow-left.svg \
+ functionalities/assets/icons/LICENSE \
+ functionalities/includes/features/class-content-tools.php \
+ functionalities/includes/features/class-link-health.php \
+ functionalities/includes/features/class-site-activity.php \
+ functionalities/includes/admin/trait-admin-utilities-ui.php \
+ functionalities/includes/admin/class-link-health-controller.php \
+ functionalities/includes/admin/class-site-activity-controller.php \
functionalities/functionalities.php \
functionalities/readme.txt \
functionalities/uninstall.php \
diff --git a/.wordpress-org/banner-1544x500.png b/.wordpress-org/banner-1544x500.png
index ec8dba5..f0b8b94 100644
Binary files a/.wordpress-org/banner-1544x500.png and b/.wordpress-org/banner-1544x500.png differ
diff --git a/.wordpress-org/banner-772x250.png b/.wordpress-org/banner-772x250.png
index 610941d..d7ca529 100644
Binary files a/.wordpress-org/banner-772x250.png and b/.wordpress-org/banner-772x250.png differ
diff --git a/.wordpress-org/banner.svg b/.wordpress-org/banner.svg
deleted file mode 100644
index 8f40f07..0000000
--- a/.wordpress-org/banner.svg
+++ /dev/null
@@ -1,248 +0,0 @@
-
diff --git a/.wordpress-org/icon-128x128.png b/.wordpress-org/icon-128x128.png
index 1507f4f..7a83638 100644
Binary files a/.wordpress-org/icon-128x128.png and b/.wordpress-org/icon-128x128.png differ
diff --git a/.wordpress-org/icon-256x256.png b/.wordpress-org/icon-256x256.png
index 03357db..eca66c6 100644
Binary files a/.wordpress-org/icon-256x256.png and b/.wordpress-org/icon-256x256.png differ
diff --git a/.wordpress-org/screenshot-1.png b/.wordpress-org/screenshot-1.png
index c8b8079..8c6346f 100644
Binary files a/.wordpress-org/screenshot-1.png and b/.wordpress-org/screenshot-1.png differ
diff --git a/.wordpress-org/screenshot-2.jpg b/.wordpress-org/screenshot-2.jpg
index 0747688..dda877e 100644
Binary files a/.wordpress-org/screenshot-2.jpg and b/.wordpress-org/screenshot-2.jpg differ
diff --git a/.wordpress-org/screenshot-3.jpg b/.wordpress-org/screenshot-3.jpg
index 8a8fc05..5308843 100644
Binary files a/.wordpress-org/screenshot-3.jpg and b/.wordpress-org/screenshot-3.jpg differ
diff --git a/.wordpress-org/screenshot-4.png b/.wordpress-org/screenshot-4.png
new file mode 100644
index 0000000..b55c338
Binary files /dev/null and b/.wordpress-org/screenshot-4.png differ
diff --git a/README.md b/README.md
index b01182c..826ff89 100644
--- a/README.md
+++ b/README.md
@@ -2,9 +2,9 @@
[](https://buymeacoffee.com/gauravtiwari)
-All-in-one WordPress optimization toolkit with 16 modules for performance, security, SEO, and content management. Built with modern WordPress coding standards and a clean module-based dashboard. Optimized for performance with modular initialization, static property caching, and intelligent transients.
+All-in-one WordPress optimization toolkit with 19 modules for performance, security, SEO, and content management. Built with modern WordPress coding standards and a clean module-based dashboard. Optimized for performance with modular initialization, static property caching, and intelligent transients.
-**Version:** 1.6.3
+**Version:** 1.7.0
**Requires WordPress:** 6.3 or later (tested up to 7.1)
**Requires PHP:** 7.4 or later
**License:** GPL-2.0-or-later
@@ -22,6 +22,12 @@ All-in-one WordPress optimization toolkit with 16 modules for performance, secur
All modules are accessed through a unified dashboard at `wp-admin/admin.php?page=functionalities`. Click any module card to configure its settings.
+Link Health shows live scan activity, a progress bar, checked-link counts, and 50 results per page across all scanned sources. Keep its workspace open for continuous bounded checking; Stop finishes the current batch and Resume continues from saved progress. See [live scan verification](docs/link-health-live-verification.md).
+
+## Backend branding and controls
+
+The admin sidebar and dashboard use a simplified vector version of the Functionalities mark that follows WordPress's UI colors. Individual module pages show their own icon, a Back to modules button, and complete breadcrumbs. Settings and module guidance use a responsive grid, with guidance in the right sidebar on wide screens and below settings on smaller screens. Module/action icons are bundled Tabler outline SVGs with their MIT license. Backend controls share consistent spacing, sizing, focus states, and accessible labels, including responsive font and PWA repeaters. See [UI verification](docs/ui-polish-verification.md) and [sidebar verification](docs/ui-sidebar-verification.md).
+
## Documentation
Full documentation is at **[functionalities.dev](https://functionalities.dev/)**:
@@ -29,7 +35,7 @@ Full documentation is at **[functionalities.dev](https://functionalities.dev/)**
| | |
|---|---|
| [Getting started](https://functionalities.dev/docs/getting-started) | Install, enable a module, verify it works |
-| [Module reference](https://functionalities.dev/modules) | What each of the 16 modules does |
+| [Module reference](https://functionalities.dev/modules) | What each of the 19 modules does |
| [Dashboard](https://functionalities.dev/docs/dashboard) | Working with the module dashboard |
| [Hooks](https://functionalities.dev/docs/hooks) | Every action and filter the plugin fires |
| [API reference](https://functionalities.dev/docs/api-reference) | Extending the plugin in code |
@@ -57,6 +63,24 @@ This plugin is built with a "Performance First" philosophy. Unlike many all-in-o
## Modules
+### Content Tools
+
+Adds **Duplicate as draft** to native post/page row actions. The new draft belongs to the current user and preserves stored block content, excerpt, taxonomies, featured image, and page template. Source identity, publication state, comments, revisions, edit locks, and scan metadata are excluded. Copying additional metadata requires the `functionalities_content_tools_meta_keys` allowlist filter and the corresponding metadata permissions. A failed copy removes the partial draft.
+
+### Link Health
+
+Checks anchor links in stored public, published posts/pages in small resumable batches. Duplicate destinations share expiring results. Safe HEAD requests use bounded GET confirmation before marking 404/410 as broken; authentication errors, rate limits, transport failures, and server errors remain inconclusive. Redirect hops are validated individually.
+
+The workspace provides manual start/resume/stop, per-link ignore/recheck, source editing links, paginated reports, and CSV export. Weekly scans are separately opt-in and use WP-Cron. Reports mark content changes, partial scans, and the 1,000 unique links per post limit. Password-protected content, dynamic output, shortcodes, and navigation are excluded. Scans never rewrite content. Results are stored in post metadata, URL checks in expiring transients, and the worker cursor in protected atomic storage.
+
+### Site Activity
+
+Stores a private history of module-setting field changes, post/page status transitions, and plugin/theme changes. Entries include actor ID, event, target, and timestamp. Setting values, snippet bodies, content, passwords, tokens, visitor IPs, and request data are excluded. The workspace supports search, event/actor filters, pagination, and explicit clearing.
+
+History is bounded to 1,000 entries and 30 days, with daily pruning. WordPress privacy export/erasure and user deletion anonymize actor references, including when recording is disabled. Logging failures leave the underlying site operation intact.
+
+All three modules are disabled by default and follow the existing uninstall retention preference.
+
### Link Management
Complete external link control with nofollow automation.
@@ -440,7 +464,7 @@ functionalities/
│ ├── core/
│ │ ├── class-module-registry.php Module list + lazy loader
│ │ └── class-wordpress-7-integration.php Abilities, DataViews, AI
-│ ├── features/ One class per module (16)
+│ ├── features/ One class per module (19)
│ ├── storage/
│ │ ├── class-atomic-json-store.php Locked, atomic JSON writes
│ │ └── class-data-directory.php Private data path + hardening
@@ -492,7 +516,7 @@ Example module definition:
## Support This Project
-Functionalities is a free and open source WordPress plugin with 16 modules for performance, security, SEO and content management, each behind its own toggle. For 1.6.0 I moved Link Management, Block Cleanup and Schema to the WordPress HTML API so they edit attributes in place and leave Vue and Alpine templates alone.
+Functionalities is a free and open source WordPress plugin with 19 modules for performance, security, SEO and content management, each behind its own toggle. For 1.6.0 I moved Link Management, Block Cleanup and Schema to the WordPress HTML API so they edit attributes in place and leave Vue and Alpine templates alone.
If it replaced a separate redirect manager or a header and footer snippets plugin on your site, you can buy me a coffee.
diff --git a/ROADMAP.md b/ROADMAP.md
index aebc832..ff9e709 100644
--- a/ROADMAP.md
+++ b/ROADMAP.md
@@ -1,3 +1,7 @@
+# 1.7.0 implementation
+
+Approved: Content Tools, Link Health, Site Activity. See [the module design](docs/plans/2026-10-02-v1.7.0-modules-design.md). All three modules are implemented and verified on Gatilab. See [verification](docs/v1.7.0-verification.md). Public release remains a separate step.
+
# Dynamic Functionalities Roadmap
The next candidate is **1.6.3**. Preserve existing options, hooks, saved content, and
diff --git a/assets/blocks/svg-icon/block.json b/assets/blocks/svg-icon/block.json
index b3330c8..7fae607 100644
--- a/assets/blocks/svg-icon/block.json
+++ b/assets/blocks/svg-icon/block.json
@@ -2,7 +2,7 @@
"$schema": "https://schemas.wp.org/trunk/block.json",
"apiVersion": 3,
"name": "functionalities/svg-icon-block",
- "version": "1.6.3",
+ "version": "1.7.0",
"title": "SVG Icon",
"category": "design",
"icon": "flag",
diff --git a/assets/brand/functionalities-admin.svg b/assets/brand/functionalities-admin.svg
new file mode 100644
index 0000000..2e98250
--- /dev/null
+++ b/assets/brand/functionalities-admin.svg
@@ -0,0 +1,13 @@
+
diff --git a/.wordpress-org/icon.svg b/assets/brand/functionalities.svg
similarity index 100%
rename from .wordpress-org/icon.svg
rename to assets/brand/functionalities.svg
diff --git a/assets/css/admin-brand.css b/assets/css/admin-brand.css
new file mode 100644
index 0000000..9f5ccae
--- /dev/null
+++ b/assets/css/admin-brand.css
@@ -0,0 +1,12 @@
+/* Keep the product mark legible in every WordPress admin color scheme. */
+#toplevel_page_functionalities .wp-menu-image::before {
+ content: "";
+ display: block;
+ width: 22px;
+ height: 22px;
+ padding: 0;
+ margin: 7px auto 0;
+ background-color: currentColor;
+ -webkit-mask: url("../brand/functionalities-admin.svg") center / contain no-repeat;
+ mask: url("../brand/functionalities-admin.svg") center / contain no-repeat;
+}
diff --git a/assets/css/admin-icons.css b/assets/css/admin-icons.css
new file mode 100644
index 0000000..e5d0a42
--- /dev/null
+++ b/assets/css/admin-icons.css
@@ -0,0 +1,116 @@
+/* Bundled Tabler outline masks; geometry sourced from ~/Icons. */
+.functionalities-icon--activity { --func-icon: url("../icons/activity.svg"); }
+.functionalities-icon--alert-triangle,
+:is(.functionalities-dashboard,.functionalities-module) .dashicons-warning::before { --func-icon: url("../icons/alert-triangle.svg"); }
+.functionalities-icon--arrow-back-up,
+:is(.functionalities-dashboard,.functionalities-module) .dashicons-editor-undo::before { --func-icon: url("../icons/arrow-back-up.svg"); }
+.functionalities-icon--arrow-right,
+:is(.functionalities-dashboard,.functionalities-module) .dashicons-arrow-right-alt2::before { --func-icon: url("../icons/arrow-right.svg"); }
+.functionalities-icon--arrow-up-right,
+:is(.functionalities-dashboard,.functionalities-module) .dashicons-external::before { --func-icon: url("../icons/arrow-up-right.svg"); }
+.functionalities-icon--arrows-shuffle,
+:is(.functionalities-dashboard,.functionalities-module) .dashicons-randomize::before { --func-icon: url("../icons/arrows-shuffle.svg"); }
+.functionalities-icon--book,
+:is(.functionalities-dashboard,.functionalities-module) .dashicons-book::before { --func-icon: url("../icons/book.svg"); }
+.functionalities-icon--braces,
+:is(.functionalities-dashboard,.functionalities-module) .dashicons-block-default::before { --func-icon: url("../icons/braces.svg"); }
+.functionalities-icon--briefcase,
+:is(.functionalities-dashboard,.functionalities-module) .dashicons-portfolio::before { --func-icon: url("../icons/briefcase.svg"); }
+.functionalities-icon--check,
+:is(.functionalities-dashboard,.functionalities-module) .dashicons-yes-alt::before,
+:is(.functionalities-dashboard,.functionalities-module) .dashicons-yes::before { --func-icon: url("../icons/check.svg"); }
+.functionalities-icon--chevron-down,
+:is(.functionalities-dashboard,.functionalities-module) .dashicons-arrow-down-alt2::before { --func-icon: url("../icons/chevron-down.svg"); }
+.functionalities-icon--chevron-up,
+:is(.functionalities-dashboard,.functionalities-module) .dashicons-arrow-up-alt2::before { --func-icon: url("../icons/chevron-up.svg"); }
+.functionalities-icon--clipboard,
+:is(.functionalities-dashboard,.functionalities-module) .dashicons-clipboard::before { --func-icon: url("../icons/clipboard.svg"); }
+.functionalities-icon--code,
+:is(.functionalities-dashboard,.functionalities-module) .dashicons-editor-code::before { --func-icon: url("../icons/code.svg"); }
+.functionalities-icon--columns-2,
+:is(.functionalities-dashboard,.functionalities-module) .dashicons-columns::before { --func-icon: url("../icons/columns-2.svg"); }
+.functionalities-icon--components,
+:is(.functionalities-dashboard,.functionalities-module) .dashicons-layout::before { --func-icon: url("../icons/components.svg"); }
+.functionalities-icon--copy,
+:is(.functionalities-dashboard,.functionalities-module) .dashicons-admin-page::before { --func-icon: url("../icons/copy.svg"); }
+.functionalities-icon--copyright,
+:is(.functionalities-dashboard,.functionalities-module) .dashicons-media-text::before { --func-icon: url("../icons/copyright.svg"); }
+.functionalities-icon--dashboard,
+:is(.functionalities-dashboard,.functionalities-module) .dashicons-dashboard::before { --func-icon: url("../icons/dashboard.svg"); }
+.functionalities-icon--database,
+:is(.functionalities-dashboard,.functionalities-module) .dashicons-database::before { --func-icon: url("../icons/database.svg"); }
+.functionalities-icon--device-floppy,
+:is(.functionalities-dashboard,.functionalities-module) .dashicons-editor-save::before { --func-icon: url("../icons/device-floppy.svg"); }
+.functionalities-icon--device-mobile,
+:is(.functionalities-dashboard,.functionalities-module) .dashicons-smartphone::before { --func-icon: url("../icons/device-mobile.svg"); }
+.functionalities-icon--download,
+:is(.functionalities-dashboard,.functionalities-module) .dashicons-download::before { --func-icon: url("../icons/download.svg"); }
+.functionalities-icon--edit,
+:is(.functionalities-dashboard,.functionalities-module) .dashicons-edit::before { --func-icon: url("../icons/edit.svg"); }
+.functionalities-icon--eye,
+:is(.functionalities-dashboard,.functionalities-module) .dashicons-visibility::before { --func-icon: url("../icons/eye.svg"); }
+.functionalities-icon--file-description,
+:is(.functionalities-dashboard,.functionalities-module) .dashicons-format-aside::before { --func-icon: url("../icons/file-description.svg"); }
+.functionalities-icon--file-plus,
+:is(.functionalities-dashboard,.functionalities-module) .dashicons-welcome-add-page::before { --func-icon: url("../icons/file-plus.svg"); }
+.functionalities-icon--filter,
+:is(.functionalities-dashboard,.functionalities-module) .dashicons-filter::before { --func-icon: url("../icons/filter.svg"); }
+.functionalities-icon--flag,
+:is(.functionalities-dashboard,.functionalities-module) .dashicons-flag::before { --func-icon: url("../icons/flag.svg"); }
+.functionalities-icon--gauge,
+:is(.functionalities-dashboard,.functionalities-module) .dashicons-performance::before { --func-icon: url("../icons/gauge.svg"); }
+.functionalities-icon--help,
+:is(.functionalities-dashboard,.functionalities-module) .dashicons-sos::before,
+:is(.functionalities-dashboard,.functionalities-module) .dashicons-editor-help::before { --func-icon: url("../icons/help.svg"); }
+.functionalities-icon--hierarchy,
+:is(.functionalities-dashboard,.functionalities-module) .dashicons-networking::before { --func-icon: url("../icons/hierarchy.svg"); }
+.functionalities-icon--icons { --func-icon: url("../icons/icons.svg"); }
+.functionalities-icon--info-circle,
+:is(.functionalities-dashboard,.functionalities-module) .dashicons-info::before,
+:is(.functionalities-dashboard,.functionalities-module) .dashicons-info-outline::before { --func-icon: url("../icons/info-circle.svg"); }
+.functionalities-icon--link,
+:is(.functionalities-dashboard,.functionalities-module) .dashicons-admin-links::before { --func-icon: url("../icons/link.svg"); }
+.functionalities-icon--list-check { --func-icon: url("../icons/list-check.svg"); }
+.functionalities-icon--list-details,
+:is(.functionalities-dashboard,.functionalities-module) .dashicons-list-view::before { --func-icon: url("../icons/list-details.svg"); }
+.functionalities-icon--lock,
+:is(.functionalities-dashboard,.functionalities-module) .dashicons-lock::before { --func-icon: url("../icons/lock.svg"); }
+.functionalities-icon--maximize,
+:is(.functionalities-dashboard,.functionalities-module) .dashicons-editor-expand::before { --func-icon: url("../icons/maximize.svg"); }
+.functionalities-icon--menu-2,
+:is(.functionalities-dashboard,.functionalities-module) .dashicons-menu::before { --func-icon: url("../icons/menu-2.svg"); }
+.functionalities-icon--minimize,
+:is(.functionalities-dashboard,.functionalities-module) .dashicons-editor-contract::before { --func-icon: url("../icons/minimize.svg"); }
+.functionalities-icon--photo,
+:is(.functionalities-dashboard,.functionalities-module) .dashicons-image::before,
+:is(.functionalities-dashboard,.functionalities-module) .dashicons-format-gallery::before,
+:is(.functionalities-dashboard,.functionalities-module) .dashicons-format-image::before { --func-icon: url("../icons/photo.svg"); }
+.functionalities-icon--plus,
+:is(.functionalities-dashboard,.functionalities-module) .dashicons-plus-alt2::before,
+:is(.functionalities-dashboard,.functionalities-module) .dashicons-plus::before { --func-icon: url("../icons/plus.svg"); }
+.functionalities-icon--refresh,
+:is(.functionalities-dashboard,.functionalities-module) .dashicons-update::before { --func-icon: url("../icons/refresh.svg"); }
+.functionalities-icon--search,
+:is(.functionalities-dashboard,.functionalities-module) .dashicons-editor-unlink::before,
+:is(.functionalities-dashboard,.functionalities-module) .dashicons-search::before { --func-icon: url("../icons/search.svg"); }
+.functionalities-icon--settings,
+:is(.functionalities-dashboard,.functionalities-module) .dashicons-admin-generic::before,
+:is(.functionalities-dashboard,.functionalities-module) .dashicons-admin-settings::before { --func-icon: url("../icons/settings.svg"); }
+.functionalities-icon--shield-check,
+:is(.functionalities-dashboard,.functionalities-module) .dashicons-shield::before { --func-icon: url("../icons/shield-check.svg"); }
+.functionalities-icon--tag,
+:is(.functionalities-dashboard,.functionalities-module) .dashicons-tag::before { --func-icon: url("../icons/tag.svg"); }
+.functionalities-icon--tool,
+:is(.functionalities-dashboard,.functionalities-module) .dashicons-admin-tools::before { --func-icon: url("../icons/tool.svg"); }
+.functionalities-icon--trash,
+:is(.functionalities-dashboard,.functionalities-module) .dashicons-trash::before { --func-icon: url("../icons/trash.svg"); }
+.functionalities-icon--typography,
+:is(.functionalities-dashboard,.functionalities-module) .dashicons-editor-textcolor::before { --func-icon: url("../icons/typography.svg"); }
+.functionalities-icon--upload,
+:is(.functionalities-dashboard,.functionalities-module) .dashicons-upload::before { --func-icon: url("../icons/upload.svg"); }
+.functionalities-icon--x,
+:is(.functionalities-dashboard,.functionalities-module) .dashicons-dismiss::before,
+:is(.functionalities-dashboard,.functionalities-module) .dashicons-no-alt::before,
+:is(.functionalities-dashboard,.functionalities-module) .dashicons-no::before { --func-icon: url("../icons/x.svg"); }
+
+.functionalities-icon--arrow-left { --func-icon: url("../icons/arrow-left.svg"); }
diff --git a/assets/css/admin-polish.css b/assets/css/admin-polish.css
new file mode 100644
index 0000000..6d74bd3
--- /dev/null
+++ b/assets/css/admin-polish.css
@@ -0,0 +1,223 @@
+/** Shared backend controls and layout. Scoped to Functionalities screens. */
+:is(.functionalities-dashboard, .functionalities-module) {
+ --func-accent: var(--wp-admin-theme-color, #2271b1);
+ --func-ink: #1d2327;
+ --func-muted: #50575e;
+ --func-line: #dcdcde;
+ --func-radius: 6px;
+ color: var(--func-ink);
+ max-width: 1380px;
+}
+.functionalities-header {
+ display: flex;
+ gap: 16px;
+ align-items: center;
+ margin: 18px 0 24px;
+}
+.functionalities-header__copy { min-width: 0; }
+.functionalities-header h1 { padding: 0; margin: 0 0 5px; font-size: 26px; line-height: 1.3; font-weight: 600; flex-wrap: wrap; }
+.functionalities-header p { margin: 0; color: var(--func-muted); font-size: 14px; line-height: 1.6; }
+.functionalities-brand-mark {
+ display: block;
+ flex: 0 0 auto;
+ color: var(--func-accent);
+ background-color: currentColor;
+ -webkit-mask: url("../brand/functionalities-admin.svg") center / contain no-repeat;
+ mask: url("../brand/functionalities-admin.svg") center / contain no-repeat;
+}
+.functionalities-version { display: inline-block; margin-inline-start: 8px; padding: 3px 8px; border: 1px solid var(--func-line); border-radius: 4px; font-size: 12px; line-height: 1.4; color: var(--func-muted); vertical-align: middle; white-space: nowrap; }
+.functionalities-icon,
+:is(.functionalities-dashboard,.functionalities-module) .dashicons:is(.dashicons-admin-page,.dashicons-admin-links,.dashicons-list-view,.dashicons-yes-alt,.dashicons-yes,.dashicons-performance,.dashicons-editor-code,.dashicons-randomize,.dashicons-block-default,.dashicons-networking,.dashicons-shield,.dashicons-visibility,.dashicons-lock,.dashicons-media-text,.dashicons-layout,.dashicons-editor-textcolor,.dashicons-editor-unlink,.dashicons-flag,.dashicons-smartphone,.dashicons-admin-generic,.dashicons-book,.dashicons-sos,.dashicons-database,.dashicons-plus-alt2,.dashicons-plus,.dashicons-search,.dashicons-edit,.dashicons-trash,.dashicons-download,.dashicons-upload,.dashicons-arrow-down-alt2,.dashicons-arrow-up-alt2,.dashicons-arrow-right-alt2,.dashicons-info,.dashicons-info-outline,.dashicons-warning,.dashicons-dismiss,.dashicons-no-alt,.dashicons-no,.dashicons-tag,.dashicons-portfolio,.dashicons-dashboard,.dashicons-update,.dashicons-external,.dashicons-image,.dashicons-filter,.dashicons-admin-settings,.dashicons-admin-tools,.dashicons-editor-undo,.dashicons-menu,.dashicons-format-gallery,.dashicons-format-image,.dashicons-editor-help,.dashicons-clipboard,.dashicons-editor-save,.dashicons-format-aside,.dashicons-editor-expand,.dashicons-editor-contract,.dashicons-columns,.dashicons-welcome-add-page)::before {
+ content: "";
+ display: inline-block;
+ width: 20px;
+ height: 20px;
+ flex: 0 0 auto;
+ background-color: currentColor;
+ -webkit-mask: var(--func-icon) center / contain no-repeat;
+ mask: var(--func-icon) center / contain no-repeat;
+ vertical-align: middle;
+}
+:is(.functionalities-dashboard,.functionalities-module) .dashicons:is(.dashicons-admin-page,.dashicons-admin-links,.dashicons-list-view,.dashicons-yes-alt,.dashicons-yes,.dashicons-performance,.dashicons-editor-code,.dashicons-randomize,.dashicons-block-default,.dashicons-networking,.dashicons-shield,.dashicons-visibility,.dashicons-lock,.dashicons-media-text,.dashicons-layout,.dashicons-editor-textcolor,.dashicons-editor-unlink,.dashicons-flag,.dashicons-smartphone,.dashicons-admin-generic,.dashicons-book,.dashicons-sos,.dashicons-database,.dashicons-plus-alt2,.dashicons-plus,.dashicons-search,.dashicons-edit,.dashicons-trash,.dashicons-download,.dashicons-upload,.dashicons-arrow-down-alt2,.dashicons-arrow-up-alt2,.dashicons-arrow-right-alt2,.dashicons-info,.dashicons-info-outline,.dashicons-warning,.dashicons-dismiss,.dashicons-no-alt,.dashicons-no,.dashicons-tag,.dashicons-portfolio,.dashicons-dashboard,.dashicons-update,.dashicons-external,.dashicons-image,.dashicons-filter,.dashicons-admin-settings,.dashicons-admin-tools,.dashicons-editor-undo,.dashicons-menu,.dashicons-format-gallery,.dashicons-format-image,.dashicons-editor-help,.dashicons-clipboard,.dashicons-editor-save,.dashicons-format-aside,.dashicons-editor-expand,.dashicons-editor-contract,.dashicons-columns,.dashicons-welcome-add-page) {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 0;
+}
+.functionalities-module-card .functionalities-icon { width: 28px; height: 28px; color: var(--func-accent); }
+.functionalities-module h1 .functionalities-icon { width: 30px; height: 30px; color: var(--func-accent); }
+.functionalities-modules-grid { gap: 16px; margin-top: 0; grid-template-columns: repeat(auto-fill,minmax(min(100%,290px),1fr)); }
+.functionalities-module-card { padding: 20px; border-color: var(--func-line); border-radius: var(--func-radius); box-shadow: none; transition: border-color 150ms ease; }
+.functionalities-module-card:hover { transform: none; box-shadow: none; border-color: var(--func-accent); }
+.module-card-header { gap: 12px; margin-bottom: 12px; align-items: center; }
+.module-card-header h2 { font-size: 17px; line-height: 1.4; }
+.module-description { margin: 0 0 20px; color: var(--func-muted); font-size: 14px; line-height: 1.6; }
+.functionalities-card-actions { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-top: auto; }
+.functionalities-status { display: inline-flex; align-items: center; gap: 5px; font-size: 12px; font-weight: 500; color: var(--func-muted); }
+.functionalities-status--active { color: #006b2e; }
+.functionalities-status .functionalities-icon { width: 16px; height: 16px; color: inherit; }
+.functionalities-module .form-table { margin-top: 0; }
+.functionalities-module .form-table > tbody > tr > th { width: 205px; padding: 20px 20px 20px 0; font-size: 14px; line-height: 1.6; }
+.functionalities-module .form-table > tbody > tr > td { padding: 16px 0; vertical-align: top; }
+.functionalities-module .form-table td p { margin-top: 8px; }
+.functionalities-module :is(input[type="text"],input[type="url"],input[type="email"],input[type="number"],input[type="password"],input[type="search"],select) {
+ box-sizing: border-box;
+ min-height: 40px;
+ margin: 0;
+ padding: 7px 12px;
+ border: 1px solid #8c8f94;
+ border-radius: 4px;
+ background-color: #fff;
+ color: var(--func-ink);
+ font: 14px/1.6 -apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;
+ vertical-align: middle;
+ max-width: 100%;
+}
+.functionalities-module select { padding-inline-end: 30px; }
+.functionalities-module .form-table :is(.regular-text,input[type="text"],input[type="url"],input[type="email"],input[type="password"],select) { width: min(100%,460px); }
+.functionalities-module .form-table :is(input.small-text,input[type="number"]) { width: 110px; }
+.functionalities-module textarea {
+ box-sizing: border-box;
+ width: 100%;
+ max-width: 100%;
+ margin: 0;
+ padding: 10px 12px;
+ border: 1px solid #8c8f94;
+ border-radius: 4px;
+ background: #fff;
+ color: var(--func-ink);
+ font-size: 14px;
+ line-height: 1.6;
+ resize: vertical;
+}
+.functionalities-module :is(input,select,textarea):focus { border-color: var(--func-accent); outline: none; box-shadow: 0 0 0 1px var(--func-accent); }
+.functionalities-module :is(input,select,textarea):disabled { background-color: #f6f7f7; color: #646970; border-color: var(--func-line); }
+.functionalities-module input::placeholder,.functionalities-module textarea::placeholder { color: #646970; opacity: 1; }
+.functionalities-module :is(input[type="checkbox"],input[type="radio"]) { flex: 0 0 auto; margin: 2px 0 0; vertical-align: text-top; }
+.functionalities-module .form-table label:has(> input[type="checkbox"]) { display: inline-flex; align-items: flex-start; gap: 8px; line-height: 1.6; }
+:is(.functionalities-module,.functionalities-dashboard) :is(.button,.button-primary,.button-secondary) { min-height: 40px; display: inline-flex; align-items: center; justify-content: center; gap: 6px; padding: 6px 14px; font-size: 13px; line-height: 1.6; vertical-align: middle; }
+:is(.functionalities-module,.functionalities-dashboard) :is(button,a,summary):focus-visible { outline: 2px solid var(--func-accent); outline-offset: 3px; }
+.functionalities-module :is(.fc-form__group,.fc-font-form__group,.functionalities-field) { min-width: 0; }
+.functionalities-module :is(.fc-form__label,.fc-font-form__label,.functionalities-field-label) { display: block; margin: 0 0 6px; font-size: 13px; font-weight: 600; line-height: 1.5; }
+.functionalities-module :is(.fc-form__row,.fc-font-form__row) { gap: 16px; align-items: start; }
+.functionalities-module :is(.fc-form__row,.fc-font-form__row,.fc-font-form__group,.fc-form__group,.functionalities-field) :is(input:not([type="checkbox"]),select,textarea) { width: 100%; max-width: 100%; }
+.functionalities-module .fc-font-form__file-row { align-items: start; gap: 8px; }
+.functionalities-module .fc-font-form__file-row input { min-width: 0; }
+.functionalities-module .fc-font-form__file-row .button { flex: 0 0 auto; margin: 0; }
+.functionalities-utilities form.functionalities-utility-action { padding: 0; border: 0; background: transparent; box-shadow: none; margin: 0 4px 4px 0; }
+.functionalities-utilities .functionalities-utility-actions { align-items: end; gap: 16px; }
+.functionalities-utility-actions > label { display: flex; flex-direction: column; gap: 6px; font-size: 13px; font-weight: 600; min-width: 0; }
+.functionalities-utility-actions > label :is(input,select) { width: 240px; max-width: 100%; font-weight: 400; }
+.functionalities-utility-actions > label input[type="number"] { width: 110px; }
+.functionalities-utility-table { border: 1px solid var(--func-line); border-radius: 4px; }
+.functionalities-utility-table .widefat { border: 0; }
+.functionalities-utility-table :is(th,td) { padding: 10px 12px; vertical-align: top; }
+.functionalities-pwa-fields { display: grid; grid-template-columns: repeat(2,minmax(0,1fr)); gap: 16px; }
+.functionalities-module .functionalities-pwa-fields .regular-text { width: 100%; }
+.functionalities-module .functionalities-pwa-fields label { display: flex; flex-direction: column; gap: 6px; min-width: 0; font-size: 13px; font-weight: 600; }
+.functionalities-module .func-fs-wrap { min-width: 0; }
+.functionalities-module .func-fs-btn { margin: 0 0 6px; min-height: 32px; padding: 3px 10px; font-size: 12px; }
+.functionalities-module .func-fs-active textarea { max-width: none; }
+@media (max-width:782px) {
+ .functionalities-header { align-items: flex-start; gap: 12px; }
+ .functionalities-header h1 { font-size: 23px; }
+ .functionalities-brand-mark { width: 48px; height: 48px; }
+ .functionalities-version { margin-inline-start: 0; margin-top: 6px; }
+ .functionalities-module .form-table > tbody > tr > th { display: block; width: auto; padding: 16px 0 6px; }
+ .functionalities-module .form-table > tbody > tr > td { display: block; padding: 0 0 16px; }
+ .functionalities-module :is(.regular-text,input[type="text"],input[type="url"],input[type="email"],input[type="search"],input[type="password"],select) { width: 100%; }
+ .functionalities-module .form-table :is(.regular-text,input[type="text"],input[type="url"],input[type="email"],input[type="password"],select) { width: 100%; }
+ .functionalities-pwa-fields { grid-template-columns: minmax(0,1fr); }
+ .functionalities-utility-actions > label { width: 100%; }
+ .functionalities-utility-actions > label :is(input,select) { width: 100%; }
+ :is(.functionalities-module,.functionalities-dashboard) :is(.button,.button-primary,.button-secondary) { min-height: 44px; }
+ .functionalities-module .fc-font-form__file-row { flex-wrap: wrap; }
+ .functionalities-module .fc-font-form__file-row input { flex-basis: 100%; }
+}
+@media (prefers-reduced-motion:reduce) {
+ :is(.functionalities-dashboard,.functionalities-module) *, :is(.functionalities-dashboard,.functionalities-module) *::before, :is(.functionalities-dashboard,.functionalities-module) *::after { transition: none !important; }
+}
+/* Match older custom action buttons to the shared control rhythm. */
+.functionalities-module :is(.fc-font-form__upload-btn,.fc-form__btn,.fc-font-form__btn) {
+ box-sizing: border-box;
+ min-height: 40px;
+ padding: 7px 12px;
+ border-radius: 4px;
+ font-size: 13px;
+ line-height: 1.6;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ gap: 6px;
+}
+.functionalities-module .fc-font-form__file-row .fc-font-form__upload-btn { flex: 0 0 auto; }
+@media (max-width:782px) {
+ .functionalities-module :is(input[type="text"],input[type="url"],input[type="email"],input[type="number"],input[type="password"],input[type="search"],select,textarea) { font-size: 16px; min-height: 44px; }
+ .functionalities-module :is(.fc-font-form__upload-btn,.fc-form__btn,.fc-font-form__btn) { min-height: 44px; }
+ .functionalities-module .fc-font-form__file-row input[type="url"] { flex: 1 0 100%; }
+ .functionalities-module label:has(>input[type="checkbox"]) { min-height: 36px; padding-block: 4px; }
+}
+.functionalities-module :is(input.code,textarea.code,.fc-font-form__input--url) {
+ font-family: ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;
+}
+
+/* Settings first, with module guidance in a separate, responsive column. */
+.functionalities-navigation { margin: 18px 0 20px; }
+.functionalities-navigation ol { display: flex; flex-wrap: wrap; align-items: center; gap: 12px; margin: 0; padding: 0; list-style: none; }
+.functionalities-navigation li { display: inline-flex; align-items: center; gap: 12px; margin: 0; min-width: 0; }
+.functionalities-navigation .separator { color: var(--func-muted); }
+.functionalities-navigation [aria-current="page"] { font-weight: 600; overflow-wrap: anywhere; }
+.functionalities-navigation .functionalities-back { color: var(--func-ink); border-color: var(--func-line); background: #fff; text-decoration: none; }
+.functionalities-navigation .functionalities-back:hover { color: var(--func-accent); border-color: var(--func-accent); }
+.functionalities-navigation + .functionalities-header { margin-top: 0; }
+.functionalities-settings-layout { display: grid; grid-template-columns: minmax(0,1fr); gap: 24px; align-items: start; }
+.functionalities-settings-content { min-width: 0; container: functionalities-settings / inline-size; }
+.functionalities-settings-content > form { margin-top: 0; }
+.functionalities-settings-content .functionalities-module-docs:empty { display: none; }
+.functionalities-settings-sidebar { min-width: 0; }
+.functionalities-settings-sidebar > h2 { margin: 0 0 12px; font-size: 14px; line-height: 1.6; }
+.functionalities-settings-sidebar .functionalities-docs-accordion { border-color: var(--func-line); border-radius: var(--func-radius); margin: 0 0 12px; }
+.functionalities-settings-sidebar .functionalities-docs-accordion summary { gap: 12px; min-height: 44px; box-sizing: border-box; color: var(--func-ink); background: #fff; line-height: 1.5; }
+.functionalities-settings-sidebar .functionalities-docs-accordion summary:hover { background: #f6f7f7; }
+.functionalities-settings-sidebar .functionalities-docs-accordion summary::after { flex: 0 0 auto; }
+.functionalities-settings-sidebar .functionalities-docs-content { color: var(--func-muted); overflow-wrap: anywhere; }
+.functionalities-settings-sidebar :is(code,pre) { white-space: normal; overflow-wrap: anywhere; }
+.functionalities-settings-sidebar .functionalities-docs-content :is(p,ul,ol,dl) + :is(p,ul,ol,dl) { margin-top: 12px; }
+.functionalities-settings-content .functionalities-utility-actions { flex-wrap: wrap; }
+.functionalities-settings-content .functionalities-utility-table { overflow-x: auto; }
+.functionalities-settings-content .functionalities-utility-table .widefat { min-width: 560px; }
+.functionalities-settings-content .functionalities-utility-table td { overflow-wrap: anywhere; }
+.functionalities-link-progress { margin-top: 24px; padding: 20px; background: #fff; border: 1px solid var(--func-line); border-radius: var(--func-radius); }
+.functionalities-link-progress h2 { margin: 0 0 12px; }
+.functionalities-link-state { display: flex; align-items: center; gap: 8px; min-height: 24px; }
+.functionalities-link-state .spinner { float: none; margin: 0; display: none; }
+.functionalities-link-state .spinner.is-active { display: inline-block; }
+.functionalities-link-progress progress { display: block; width: 100%; height: 10px; accent-color: var(--func-accent); }
+.functionalities-link-progress [data-link-error] { color: #b32d2e; }
+.functionalities-link-progress [hidden] { display: none !important; }
+.functionalities-link-report { margin-top: 28px; min-width: 0; }
+.functionalities-link-report .functionalities-utility-table { overflow-x: auto; }
+.functionalities-link-report .widefat { min-width: 780px; table-layout: auto; }
+.functionalities-link-report :is(th,td):first-child { min-width: 120px; width: 16%; }
+.functionalities-link-report .functionalities-utility-url { min-width: 240px; overflow-wrap: anywhere; }
+.functionalities-link-report :is(th,td):nth-child(3) { min-width: 100px; white-space: nowrap; }
+.functionalities-link-report :is(th,td):nth-child(4) { min-width: 135px; }
+.functionalities-link-report :is(th,td):last-child { min-width: 100px; }
+@media (prefers-reduced-motion:reduce) { .functionalities-link-state .spinner { animation: none; } }
+@media (min-width:1180px) {
+ .functionalities-settings-layout--with-guide { grid-template-columns: minmax(0,1fr) 320px; }
+}
+@container functionalities-settings (max-width:620px) {
+ .functionalities-module .form-table > tbody > tr > th { display: block; width: auto; padding: 16px 0 6px; }
+ .functionalities-module .form-table > tbody > tr > td { display: block; padding: 0 0 16px; }
+ .functionalities-module :is(.fc-form__row,.fc-font-form__row) { grid-template-columns: minmax(0,1fr); }
+ .functionalities-module .fc-font-form__file-row { flex-wrap: wrap; }
+ .functionalities-module .fc-font-form__file-row input[type="url"] { flex: 1 0 100%; }
+}
+@media (max-width:782px) {
+ .functionalities-navigation .functionalities-back { min-height: 44px; }
+ .functionalities-settings-content > form { padding: 16px; }
+ .functionalities-settings-sidebar .functionalities-docs-content { font-size: 14px; }
+ .functionalities-settings-content .functionalities-utility-actions > label { width: 100%; }
+ .functionalities-settings-content .functionalities-utility-actions > label :is(input,select) { width: 100%; }
+}
diff --git a/assets/css/admin.css b/assets/css/admin.css
index c398a37..c578d5d 100644
--- a/assets/css/admin.css
+++ b/assets/css/admin.css
@@ -1582,3 +1582,19 @@
background: #f6f7f7;
white-space: pre-wrap;
}
+
+/* Utility workspaces use native controls and scroll independently on small screens. */
+.functionalities-utility-actions { display: flex; gap: 12px; flex-wrap: wrap; align-items: end; margin: 16px 0; }
+.functionalities-utility-action { display: inline-block; margin: 3px; }
+.functionalities-utility-table { overflow-x: auto; margin: 16px 0; }
+.functionalities-utility-table table { min-width: 650px; }
+.functionalities-utility-url { overflow-wrap: anywhere; max-width: 440px; }
+.functionalities-utilities form.functionalities-utility-action {
+ padding: 0;
+ margin: 0 4px 4px 0;
+ border: 0;
+ border-radius: 0;
+ background: transparent;
+ box-shadow: none;
+}
+.functionalities-utilities .functionalities-utility-table td { vertical-align: top; }
diff --git a/assets/icons/LICENSE b/assets/icons/LICENSE
new file mode 100644
index 0000000..3e82379
--- /dev/null
+++ b/assets/icons/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2020-2026 Paweł Kuna
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/assets/icons/activity.svg b/assets/icons/activity.svg
new file mode 100644
index 0000000..985b527
--- /dev/null
+++ b/assets/icons/activity.svg
@@ -0,0 +1,4 @@
+
diff --git a/assets/icons/alert-triangle.svg b/assets/icons/alert-triangle.svg
new file mode 100644
index 0000000..f96052e
--- /dev/null
+++ b/assets/icons/alert-triangle.svg
@@ -0,0 +1,6 @@
+
diff --git a/assets/icons/arrow-back-up.svg b/assets/icons/arrow-back-up.svg
new file mode 100644
index 0000000..7a374c4
--- /dev/null
+++ b/assets/icons/arrow-back-up.svg
@@ -0,0 +1,5 @@
+
diff --git a/assets/icons/arrow-left.svg b/assets/icons/arrow-left.svg
new file mode 100644
index 0000000..d6440c9
--- /dev/null
+++ b/assets/icons/arrow-left.svg
@@ -0,0 +1,16 @@
+
\ No newline at end of file
diff --git a/assets/icons/arrow-right.svg b/assets/icons/arrow-right.svg
new file mode 100644
index 0000000..29ad3f0
--- /dev/null
+++ b/assets/icons/arrow-right.svg
@@ -0,0 +1,6 @@
+
diff --git a/assets/icons/arrow-up-right.svg b/assets/icons/arrow-up-right.svg
new file mode 100644
index 0000000..c2084a7
--- /dev/null
+++ b/assets/icons/arrow-up-right.svg
@@ -0,0 +1,5 @@
+
diff --git a/assets/icons/arrows-shuffle.svg b/assets/icons/arrows-shuffle.svg
new file mode 100644
index 0000000..87ae9f9
--- /dev/null
+++ b/assets/icons/arrows-shuffle.svg
@@ -0,0 +1,7 @@
+
diff --git a/assets/icons/book.svg b/assets/icons/book.svg
new file mode 100644
index 0000000..b39c439
--- /dev/null
+++ b/assets/icons/book.svg
@@ -0,0 +1,8 @@
+
diff --git a/assets/icons/braces.svg b/assets/icons/braces.svg
new file mode 100644
index 0000000..e9edb35
--- /dev/null
+++ b/assets/icons/braces.svg
@@ -0,0 +1,5 @@
+
diff --git a/assets/icons/briefcase.svg b/assets/icons/briefcase.svg
new file mode 100644
index 0000000..8f7328e
--- /dev/null
+++ b/assets/icons/briefcase.svg
@@ -0,0 +1,7 @@
+
diff --git a/assets/icons/check.svg b/assets/icons/check.svg
new file mode 100644
index 0000000..a700f10
--- /dev/null
+++ b/assets/icons/check.svg
@@ -0,0 +1,4 @@
+
diff --git a/assets/icons/chevron-down.svg b/assets/icons/chevron-down.svg
new file mode 100644
index 0000000..68a1bf3
--- /dev/null
+++ b/assets/icons/chevron-down.svg
@@ -0,0 +1,4 @@
+
diff --git a/assets/icons/chevron-up.svg b/assets/icons/chevron-up.svg
new file mode 100644
index 0000000..15a6bc0
--- /dev/null
+++ b/assets/icons/chevron-up.svg
@@ -0,0 +1,4 @@
+
diff --git a/assets/icons/clipboard.svg b/assets/icons/clipboard.svg
new file mode 100644
index 0000000..29698ff
--- /dev/null
+++ b/assets/icons/clipboard.svg
@@ -0,0 +1,5 @@
+
diff --git a/assets/icons/code.svg b/assets/icons/code.svg
new file mode 100644
index 0000000..7c20fae
--- /dev/null
+++ b/assets/icons/code.svg
@@ -0,0 +1,6 @@
+
diff --git a/assets/icons/columns-2.svg b/assets/icons/columns-2.svg
new file mode 100644
index 0000000..3f31848
--- /dev/null
+++ b/assets/icons/columns-2.svg
@@ -0,0 +1,5 @@
+
diff --git a/assets/icons/components.svg b/assets/icons/components.svg
new file mode 100644
index 0000000..60c4889
--- /dev/null
+++ b/assets/icons/components.svg
@@ -0,0 +1,7 @@
+
diff --git a/assets/icons/copy.svg b/assets/icons/copy.svg
new file mode 100644
index 0000000..42e9041
--- /dev/null
+++ b/assets/icons/copy.svg
@@ -0,0 +1,5 @@
+
diff --git a/assets/icons/copyright.svg b/assets/icons/copyright.svg
new file mode 100644
index 0000000..88b4a33
--- /dev/null
+++ b/assets/icons/copyright.svg
@@ -0,0 +1,5 @@
+
diff --git a/assets/icons/dashboard.svg b/assets/icons/dashboard.svg
new file mode 100644
index 0000000..fcac0b7
--- /dev/null
+++ b/assets/icons/dashboard.svg
@@ -0,0 +1,6 @@
+
diff --git a/assets/icons/database.svg b/assets/icons/database.svg
new file mode 100644
index 0000000..d97ae7a
--- /dev/null
+++ b/assets/icons/database.svg
@@ -0,0 +1,6 @@
+
diff --git a/assets/icons/device-floppy.svg b/assets/icons/device-floppy.svg
new file mode 100644
index 0000000..372be41
--- /dev/null
+++ b/assets/icons/device-floppy.svg
@@ -0,0 +1,6 @@
+
diff --git a/assets/icons/device-mobile.svg b/assets/icons/device-mobile.svg
new file mode 100644
index 0000000..9ad4072
--- /dev/null
+++ b/assets/icons/device-mobile.svg
@@ -0,0 +1,6 @@
+
diff --git a/assets/icons/download.svg b/assets/icons/download.svg
new file mode 100644
index 0000000..8cc8c8f
--- /dev/null
+++ b/assets/icons/download.svg
@@ -0,0 +1,6 @@
+
diff --git a/assets/icons/edit.svg b/assets/icons/edit.svg
new file mode 100644
index 0000000..f530f0d
--- /dev/null
+++ b/assets/icons/edit.svg
@@ -0,0 +1,6 @@
+
diff --git a/assets/icons/eye.svg b/assets/icons/eye.svg
new file mode 100644
index 0000000..5b33256
--- /dev/null
+++ b/assets/icons/eye.svg
@@ -0,0 +1,5 @@
+
diff --git a/assets/icons/file-description.svg b/assets/icons/file-description.svg
new file mode 100644
index 0000000..b8d2cad
--- /dev/null
+++ b/assets/icons/file-description.svg
@@ -0,0 +1,7 @@
+
diff --git a/assets/icons/file-plus.svg b/assets/icons/file-plus.svg
new file mode 100644
index 0000000..8ce2f57
--- /dev/null
+++ b/assets/icons/file-plus.svg
@@ -0,0 +1,7 @@
+
diff --git a/assets/icons/filter.svg b/assets/icons/filter.svg
new file mode 100644
index 0000000..fb3ab47
--- /dev/null
+++ b/assets/icons/filter.svg
@@ -0,0 +1,4 @@
+
diff --git a/assets/icons/flag.svg b/assets/icons/flag.svg
new file mode 100644
index 0000000..0959bf0
--- /dev/null
+++ b/assets/icons/flag.svg
@@ -0,0 +1,5 @@
+
diff --git a/assets/icons/gauge.svg b/assets/icons/gauge.svg
new file mode 100644
index 0000000..d250d2e
--- /dev/null
+++ b/assets/icons/gauge.svg
@@ -0,0 +1,7 @@
+
diff --git a/assets/icons/help.svg b/assets/icons/help.svg
new file mode 100644
index 0000000..e029dac
--- /dev/null
+++ b/assets/icons/help.svg
@@ -0,0 +1,6 @@
+
diff --git a/assets/icons/hierarchy.svg b/assets/icons/hierarchy.svg
new file mode 100644
index 0000000..16641b8
--- /dev/null
+++ b/assets/icons/hierarchy.svg
@@ -0,0 +1,8 @@
+
diff --git a/assets/icons/icons.svg b/assets/icons/icons.svg
new file mode 100644
index 0000000..6c46c10
--- /dev/null
+++ b/assets/icons/icons.svg
@@ -0,0 +1,8 @@
+
diff --git a/assets/icons/info-circle.svg b/assets/icons/info-circle.svg
new file mode 100644
index 0000000..e2e247b
--- /dev/null
+++ b/assets/icons/info-circle.svg
@@ -0,0 +1,6 @@
+
diff --git a/assets/icons/link.svg b/assets/icons/link.svg
new file mode 100644
index 0000000..14f1dce
--- /dev/null
+++ b/assets/icons/link.svg
@@ -0,0 +1,6 @@
+
diff --git a/assets/icons/list-check.svg b/assets/icons/list-check.svg
new file mode 100644
index 0000000..53786d0
--- /dev/null
+++ b/assets/icons/list-check.svg
@@ -0,0 +1,9 @@
+
diff --git a/assets/icons/list-details.svg b/assets/icons/list-details.svg
new file mode 100644
index 0000000..97658bd
--- /dev/null
+++ b/assets/icons/list-details.svg
@@ -0,0 +1,9 @@
+
diff --git a/assets/icons/lock.svg b/assets/icons/lock.svg
new file mode 100644
index 0000000..c406543
--- /dev/null
+++ b/assets/icons/lock.svg
@@ -0,0 +1,6 @@
+
diff --git a/assets/icons/maximize.svg b/assets/icons/maximize.svg
new file mode 100644
index 0000000..7d6695c
--- /dev/null
+++ b/assets/icons/maximize.svg
@@ -0,0 +1,7 @@
+
diff --git a/assets/icons/menu-2.svg b/assets/icons/menu-2.svg
new file mode 100644
index 0000000..306f7f0
--- /dev/null
+++ b/assets/icons/menu-2.svg
@@ -0,0 +1,6 @@
+
diff --git a/assets/icons/minimize.svg b/assets/icons/minimize.svg
new file mode 100644
index 0000000..dc38c63
--- /dev/null
+++ b/assets/icons/minimize.svg
@@ -0,0 +1,7 @@
+
diff --git a/assets/icons/photo.svg b/assets/icons/photo.svg
new file mode 100644
index 0000000..724f5fb
--- /dev/null
+++ b/assets/icons/photo.svg
@@ -0,0 +1,7 @@
+
diff --git a/assets/icons/plus.svg b/assets/icons/plus.svg
new file mode 100644
index 0000000..94ec277
--- /dev/null
+++ b/assets/icons/plus.svg
@@ -0,0 +1,5 @@
+
diff --git a/assets/icons/refresh.svg b/assets/icons/refresh.svg
new file mode 100644
index 0000000..10434b3
--- /dev/null
+++ b/assets/icons/refresh.svg
@@ -0,0 +1,5 @@
+
diff --git a/assets/icons/search.svg b/assets/icons/search.svg
new file mode 100644
index 0000000..5b43178
--- /dev/null
+++ b/assets/icons/search.svg
@@ -0,0 +1,5 @@
+
diff --git a/assets/icons/settings.svg b/assets/icons/settings.svg
new file mode 100644
index 0000000..c58897f
--- /dev/null
+++ b/assets/icons/settings.svg
@@ -0,0 +1,5 @@
+
diff --git a/assets/icons/shield-check.svg b/assets/icons/shield-check.svg
new file mode 100644
index 0000000..d92ba68
--- /dev/null
+++ b/assets/icons/shield-check.svg
@@ -0,0 +1,5 @@
+
diff --git a/assets/icons/tag.svg b/assets/icons/tag.svg
new file mode 100644
index 0000000..5f40799
--- /dev/null
+++ b/assets/icons/tag.svg
@@ -0,0 +1,5 @@
+
diff --git a/assets/icons/tool.svg b/assets/icons/tool.svg
new file mode 100644
index 0000000..24441c4
--- /dev/null
+++ b/assets/icons/tool.svg
@@ -0,0 +1,4 @@
+
diff --git a/assets/icons/trash.svg b/assets/icons/trash.svg
new file mode 100644
index 0000000..0810c35
--- /dev/null
+++ b/assets/icons/trash.svg
@@ -0,0 +1,8 @@
+
diff --git a/assets/icons/typography.svg b/assets/icons/typography.svg
new file mode 100644
index 0000000..93b9880
--- /dev/null
+++ b/assets/icons/typography.svg
@@ -0,0 +1,8 @@
+
diff --git a/assets/icons/upload.svg b/assets/icons/upload.svg
new file mode 100644
index 0000000..6764e04
--- /dev/null
+++ b/assets/icons/upload.svg
@@ -0,0 +1,6 @@
+
diff --git a/assets/icons/x.svg b/assets/icons/x.svg
new file mode 100644
index 0000000..6d6f593
--- /dev/null
+++ b/assets/icons/x.svg
@@ -0,0 +1,5 @@
+
diff --git a/assets/js/admin-link-health.js b/assets/js/admin-link-health.js
new file mode 100644
index 0000000..97ee41f
--- /dev/null
+++ b/assets/js/admin-link-health.js
@@ -0,0 +1,148 @@
+/* Live progress uses bounded, nonce-protected batches and never overlaps its own workers. */
+(function () {
+ 'use strict';
+ const panel = document.querySelector('[data-link-health]');
+ if (!panel || typeof functionalitiesLinkHealth === 'undefined') return;
+ const config = functionalitiesLinkHealth;
+ const root = panel.closest('.functionalities-module');
+ const report = root.querySelector('[data-link-report]');
+ const label = panel.querySelector('[data-link-label]');
+ const summary = panel.querySelector('[data-link-summary]');
+ const meter = panel.querySelector('[data-link-meter]');
+ const spinner = panel.querySelector('[data-link-spinner]');
+ const error = panel.querySelector('[data-link-error]');
+ let state = JSON.parse(panel.dataset.progress);
+ let epoch = 0;
+ let worker = false;
+ let action = false;
+ let polling = false;
+ let timer = null;
+ let suspended = false;
+ let autoDrive = true;
+ let actionError = false;
+
+ function render() {
+ const active = state.status === 'running' || state.status === 'stopping';
+ if (label.textContent !== state.label) label.textContent = state.label;
+ summary.textContent = state.summary;
+ meter.value = state.percent;
+ spinner.classList.toggle('is-active', state.phase === 'checking' || state.phase === 'stopping' || worker);
+ panel.dataset.phase = worker && state.status === 'running' && state.phase !== 'stopping' ? 'checking' : state.phase;
+ panel.querySelector('[data-link-control="start"]').hidden = active || state.status === 'stopped';
+ panel.querySelector('[data-link-control="resume"]').hidden = state.status !== 'stopped' && state.phase !== 'error' && !(state.status === 'running' && !autoDrive);
+ panel.querySelector('[data-link-control="stop"]').hidden = !active;
+ panel.querySelector('[data-link-control="resume"] button').textContent = state.phase === 'error' ? config.retry : config.resume;
+ root.querySelectorAll('[data-link-action] button').forEach(button => {
+ const operation = button.closest('form').dataset.linkAction;
+ button.disabled = operation !== 'export' && (action || state.phase === 'disabled' || (operation !== 'stop' && worker) || (operation === 'stop' && state.status === 'stopping'));
+ });
+ }
+ function showError(message, sticky = false) {
+ if (sticky) actionError = true;
+ error.textContent = message;
+ error.hidden = false;
+ }
+ function page() {
+ return report?.querySelector('[data-link-page]')?.dataset.linkPage || '1';
+ }
+ async function request(operation, fields = {}) {
+ const body = new FormData();
+ Object.entries({action: 'functionalities_link_health_live', nonce: config.nonce, operation, run: state.run, report: '1', report_page: page(), ...fields}).forEach(([key, value]) => body.append(key, value));
+ const abort = new AbortController();
+ const timeout = setTimeout(() => abort.abort(), 25000);
+ try {
+ const response = await fetch(config.ajaxUrl, {method: 'POST', credentials: 'same-origin', body, signal: abort.signal});
+ const result = await response.json();
+ if (!result.success) {
+ const failure = new Error(result.data?.message || config.requestError);
+ failure.denied = response.status === 403;
+ throw failure;
+ }
+ return result.data;
+ } finally { clearTimeout(timeout); }
+ }
+ function accept(data) {
+ state = data.progress;
+ if (report && typeof data.html === 'string' && report.innerHTML !== data.html) report.innerHTML = data.html;
+ if (!actionError) error.hidden = true;
+ render();
+ }
+ function schedule(delay = (state.status === 'running' || state.status === 'stopping' ? 2000 : 15000)) {
+ clearTimeout(timer);
+ if (!suspended && document.visibilityState !== 'hidden') timer = setTimeout(tick, delay);
+ }
+ async function tick() {
+ if (polling || worker || action || suspended || document.visibilityState === 'hidden') return;
+ polling = true;
+ const revision = epoch;
+ try {
+ const data = await request('status');
+ if (revision === epoch) {
+ accept(data);
+ if (state.status === 'running' && state.phase === 'waiting' && !worker && autoDrive) runBatch();
+ }
+ } catch (failure) {
+ if (revision === epoch) {
+ suspended = !!failure.denied;
+ showError(failure.denied ? failure.message : config.connectionError);
+ }
+ } finally { polling = false; schedule(); }
+ }
+ async function runBatch() {
+ if (worker || action || !autoDrive || state.status !== 'running' || suspended || document.visibilityState === 'hidden') return;
+ worker = true;
+ const revision = epoch;
+ state.label = config.checking;
+ render();
+ try {
+ const data = await request('batch');
+ if (revision === epoch) accept(data);
+ } catch (failure) {
+ if (revision === epoch) {
+ suspended = !!failure.denied;
+ showError(failure.denied ? failure.message : config.connectionError);
+ }
+ } finally { worker = false; render(); schedule(1000); }
+ }
+ root.addEventListener('submit', async event => {
+ const form = event.target.closest('[data-link-action]');
+ if (!form || form.dataset.linkAction === 'export') return;
+ event.preventDefault();
+ if (action) return;
+ const operation = form.dataset.linkAction;
+ if (worker && operation !== 'stop') return;
+ const revision = ++epoch;
+ action = true;
+ clearTimeout(timer);
+ error.hidden = true;
+ actionError = false;
+ if (operation === 'stop') {
+ autoDrive = false;
+ state.label = config.stopping;
+ state.phase = 'stopping';
+ } else if (operation === 'start' || operation === 'resume') {
+ state.label = config.checking;
+ state.phase = 'checking';
+ }
+ render();
+ try {
+ const fields = Object.fromEntries(new FormData(form));
+ delete fields.action;
+ delete fields.operation;
+ const data = await request(operation, fields);
+ if (revision === epoch) {
+ suspended = false;
+ if (operation === 'start' || operation === 'resume') autoDrive = true;
+ accept(data);
+ }
+ } catch (failure) {
+ if (revision === epoch) { suspended = !!failure.denied; showError(failure.message || config.requestError, true); }
+ } finally { action = false; render(); schedule(1000); }
+ });
+ document.addEventListener('visibilitychange', () => {
+ if (document.visibilityState === 'hidden') clearTimeout(timer);
+ else schedule(0);
+ });
+ render();
+ schedule(0);
+})();
diff --git a/assets/js/admin-polish.js b/assets/js/admin-polish.js
new file mode 100644
index 0000000..2817cdb
--- /dev/null
+++ b/assets/js/admin-polish.js
@@ -0,0 +1,38 @@
+/** Associate legacy/repeater labels and keep decorative icons out of field names. */
+(function () {
+ 'use strict';
+ let nextId = 0;
+ function normalize(root) {
+ root.querySelectorAll('.dashicons').forEach(icon => icon.setAttribute('aria-hidden', 'true'));
+ root.querySelectorAll('input:not([type="hidden"]), select, textarea').forEach(control => {
+ if (control.labels && control.labels.length) return;
+ const group = control.closest('.fc-form__group, .fc-font-form__group, .functionalities-field');
+ const label = group ? group.querySelector('label') : null;
+ if (label && !label.control && group.querySelectorAll('input:not([type="hidden"]), select, textarea').length === 1) {
+ if (!control.id) {
+ do { control.id = 'functionalities-field-' + (++nextId); }
+ while (document.querySelectorAll('#' + control.id).length > 1);
+ }
+ label.htmlFor = control.id;
+ return;
+ }
+ if (!control.hasAttribute('aria-label') && !control.hasAttribute('aria-labelledby')) {
+ const row = control.closest('tr');
+ const heading = row ? row.querySelector('th') : null;
+ if (heading && row.querySelectorAll('input:not([type="hidden"]), select, textarea').length === 1) {
+ control.setAttribute('aria-label', heading.textContent.trim());
+ }
+ }
+ });
+ }
+ function init() {
+ const roots = document.querySelectorAll('.functionalities-module, .functionalities-dashboard');
+ roots.forEach(root => {
+ normalize(root);
+ const observer = new MutationObserver(() => normalize(root));
+ observer.observe(root, {childList: true, subtree: true});
+ });
+ }
+ if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
+ else init();
+})();
diff --git a/build.sh b/build.sh
index 95d7cbb..746c808 100755
--- a/build.sh
+++ b/build.sh
@@ -39,6 +39,25 @@ rsync -a \
# Fail loudly if something the plugin needs at runtime did not make it in.
REQUIRED=(
+ "includes/admin/class-admin-icons.php"
+ "assets/brand/functionalities.svg"
+ "assets/brand/functionalities-admin.svg"
+ "assets/css/admin-brand.css"
+ "assets/css/admin-icons.css"
+ "assets/css/admin-polish.css"
+ "assets/js/admin-polish.js"
+ "assets/js/admin-link-health.js"
+ "assets/icons/settings.svg"
+ "assets/icons/arrow-left.svg"
+ "assets/icons/LICENSE"
+
+ "includes/features/class-content-tools.php"
+ "includes/features/class-link-health.php"
+ "includes/features/class-site-activity.php"
+ "includes/admin/trait-admin-utilities-ui.php"
+ "includes/admin/class-link-health-controller.php"
+ "includes/admin/class-site-activity-controller.php"
+
"functionalities.php"
"index.php"
"readme.txt"
diff --git a/docs/admin-icon-sources.json b/docs/admin-icon-sources.json
new file mode 100644
index 0000000..3954f08
--- /dev/null
+++ b/docs/admin-icon-sources.json
@@ -0,0 +1,96 @@
+{
+ "family": "Tabler outline",
+ "source": "~/Icons/svg/outline",
+ "license": "MIT",
+ "module_icons": {
+ "content-tools": "copy",
+ "link-health": "link",
+ "site-activity": "activity",
+ "task-manager": "list-check",
+ "misc": "gauge",
+ "snippets": "code",
+ "link-management": "link",
+ "redirect-manager": "arrows-shuffle",
+ "block-cleanup": "braces",
+ "schema": "hierarchy",
+ "content-regression": "shield-check",
+ "assumption-detection": "eye",
+ "login-security": "lock",
+ "meta": "copyright",
+ "components": "components",
+ "fonts": "typography",
+ "editor-links": "search",
+ "svg-icons": "icons",
+ "pwa": "device-mobile"
+ },
+ "dashicons": {
+ "admin-page": "copy",
+ "admin-links": "link",
+ "list-view": "list-details",
+ "yes-alt": "check",
+ "yes": "check",
+ "performance": "gauge",
+ "editor-code": "code",
+ "randomize": "arrows-shuffle",
+ "block-default": "braces",
+ "networking": "hierarchy",
+ "shield": "shield-check",
+ "visibility": "eye",
+ "lock": "lock",
+ "media-text": "copyright",
+ "layout": "components",
+ "editor-textcolor": "typography",
+ "editor-unlink": "search",
+ "flag": "flag",
+ "smartphone": "device-mobile",
+ "admin-generic": "settings",
+ "book": "book",
+ "sos": "help",
+ "database": "database",
+ "plus-alt2": "plus",
+ "plus": "plus",
+ "search": "search",
+ "edit": "edit",
+ "trash": "trash",
+ "download": "download",
+ "upload": "upload",
+ "arrow-down-alt2": "chevron-down",
+ "arrow-up-alt2": "chevron-up",
+ "arrow-right-alt2": "arrow-right",
+ "info": "info-circle",
+ "info-outline": "info-circle",
+ "warning": "alert-triangle",
+ "dismiss": "x",
+ "no-alt": "x",
+ "no": "x",
+ "tag": "tag",
+ "portfolio": "briefcase",
+ "dashboard": "dashboard",
+ "update": "refresh",
+ "external": "arrow-up-right",
+ "image": "photo",
+ "filter": "filter",
+ "admin-settings": "settings",
+ "admin-tools": "tool",
+ "editor-undo": "arrow-back-up",
+ "menu": "menu-2",
+ "format-gallery": "photo",
+ "format-image": "photo",
+ "editor-help": "help",
+ "clipboard": "clipboard",
+ "editor-save": "device-floppy",
+ "format-aside": "file-description",
+ "editor-expand": "maximize",
+ "editor-contract": "minimize",
+ "columns": "columns-2",
+ "welcome-add-page": "file-plus"
+ },
+ "navigation_icons": {
+ "back": "arrow-left"
+ },
+ "admin_brand": {
+ "source": "assets/brand/functionalities-admin.svg",
+ "design": "Original monoline simplification of the official four-module platform mark",
+ "color": "currentColor via CSS masks; no raster payload"
+ }
+}
diff --git a/docs/branding-assets.md b/docs/branding-assets.md
new file mode 100644
index 0000000..ddf2019
--- /dev/null
+++ b/docs/branding-assets.md
@@ -0,0 +1,37 @@
+# Liquid-glass listing assets for 1.7.0
+
+The WordPress listing icon and bento banner use the approved four-color modular-platform identity with restrained, Apple-inspired Liquid Glass styling: thin translucent surfaces, shallow relief, fine edge highlights, soft ambient shadows, and gentle refraction. The four logo tiles retain softer violet, teal, amber, and blue identities above two blue platform layers. Transparent corners remain around the near-white icon tile.
+
+The bento banner retains the product name, 19-module count, and real feature groups: Content Tools, Link Health, reusable fonts/styles, and Site Activity. A light pearl product panel and nearly clear feature panels sit over a soft amber, mint, lilac, and blue background wash. Dark text, calmer heading weight, and crisp monoline glyphs keep the content primary. All copy was visually checked after generation and again at the actual 772px listing width. The material direction follows [Apple's Liquid Glass reference](https://www.apple.com/newsroom/2025/06/apple-introduces-a-delightful-and-elegant-new-software-design/); these are static marketing images, not an implementation of Apple's rendering APIs.
+
+The materials were rendered with Codex's built-in image-generation tool from the approved flat listing assets. The glass icon was then provided as the material and identity reference for the banner. Native selected renders and exact prompts are preserved in `docs/branding-sources/`; the earlier editable flat SVGs remain there as identity/layout references. The runtime admin mark remains the color-adaptive vector at `assets/brand/functionalities-admin.svg`.
+
+## Listing exports
+
+| File in `.wordpress-org/` | Purpose | Pixels |
+|---|---|---|
+| `icon-128x128.png` | Standard listing icon | 128 x 128 |
+| `icon-256x256.png` | Retina listing icon | 256 x 256 |
+| `banner-772x250.png` | Standard bento banner | 772 x 250 |
+| `banner-1544x500.png` | Retina bento banner | 1544 x 500 |
+
+The exact filenames and pixel dimensions follow the [WordPress Plugin Handbook](https://developer.wordpress.org/plugins/wordpress-org/plugin-assets/). Listing SVGs were moved into the source archive so WordPress does not prefer the earlier flat SVG over the new glass PNGs. The configured 10up deployment action syncs its assets directory with deletion, so the superseded listing SVG is removed during the next public release.
+
+## Verification and reproduction
+
+- Inspected both native renders, both icon delivery sizes, and both banner sizes. The icon was also composited on white and dark backgrounds for alpha/edge inspection.
+- PNG signatures, exact dimensions, transparent icon corners, and WordPress file-size limits passed. Icons are below 81KB and banners below 685KB.
+- The native banner is 2203 x 714; the native icon is 1254 x 1254. Listing exports are downsampled, with no enlargement. The banner's native ratio differs from the exact listing ratio by less than 0.1%.
+- The brand silhouette and all feature labels remain recognizable. Generation adds material detail; the archived flat SVGs remain the editable geometry and typography references rather than being presented as the source of the raster glass effects.
+- No runtime PHP, JavaScript, CSS, settings, or frontend behavior changed in this asset pass.
+
+Export from the selected native renders with ImageMagick:
+
+```sh
+magick docs/branding-sources/icon-liquid-glass.png -filter Lanczos -resize 128x128 .wordpress-org/icon-128x128.png
+magick docs/branding-sources/icon-liquid-glass.png -filter Lanczos -resize 256x256 .wordpress-org/icon-256x256.png
+magick docs/branding-sources/banner-liquid-glass.png -filter Lanczos -resize 772x250! .wordpress-org/banner-772x250.png
+magick docs/branding-sources/banner-liquid-glass.png -filter Lanczos -resize 1544x500! .wordpress-org/banner-1544x500.png
+```
+
+These are prepared release assets. Public WordPress.org deployment remains separate from the tested Gatilab runtime candidate.
diff --git a/docs/branding-sources/banner-flat.svg b/docs/branding-sources/banner-flat.svg
new file mode 100644
index 0000000..c6922b3
--- /dev/null
+++ b/docs/branding-sources/banner-flat.svg
@@ -0,0 +1,54 @@
+
diff --git a/docs/branding-sources/banner-liquid-glass.png b/docs/branding-sources/banner-liquid-glass.png
new file mode 100644
index 0000000..7789952
Binary files /dev/null and b/docs/branding-sources/banner-liquid-glass.png differ
diff --git a/docs/branding-sources/icon-flat.svg b/docs/branding-sources/icon-flat.svg
new file mode 100644
index 0000000..e6286b1
--- /dev/null
+++ b/docs/branding-sources/icon-flat.svg
@@ -0,0 +1,13 @@
+
' . \esc_html( $description ) . '
'; + } + echo '' . esc_html( $description ) . '
', - 'usage' + 'usage', + true ); } @@ -114,7 +189,9 @@ public static function render_caution_docs( string $message ): void { * @return void */ public static function render_module_docs( array $config ): void { - echo '- -
+