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 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Dynamic - - - Functionalities - - - - All-in-one WordPress optimization toolkit - - - - - - 16 Modules - - - Modular - - - Zero Bloat - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 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 @@ [![Buy me a coffee](https://img.shields.io/badge/Buy%20me%20a%20coffee-FFDD00?style=flat&logo=buymeacoffee&logoColor=black)](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 @@ + + Functionalities modular platform + Four modular tiles above two platform layers, simplified for small WordPress admin icons. + + + + + + + + + + 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 @@ + + Dynamic Functionalities: a modular WordPress toolkit + A colorful bento banner with the Functionalities platform mark, 19 optional modules, and tiles for Content Tools, Link Health, reusable fonts and styles, and Site Activity. Free and open source. + + + + + + + + WordPress toolkit + Dynamic + Functionalities + Enable only what you need. + 19 optional modules + Free and open source + + + + + + + + + + + + + + + + + + + + Content Tools + Duplicate into drafts + Link Health + Find broken links + Fonts & styles + Built for reuse + Site Activity + See what changed + 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 @@ + + Dynamic Functionalities + Four colorful modular tiles above blue platform layers, matching the Functionalities admin mark. + + + + + + + + + + diff --git a/docs/branding-sources/icon-liquid-glass.png b/docs/branding-sources/icon-liquid-glass.png new file mode 100644 index 0000000..b11e5e4 Binary files /dev/null and b/docs/branding-sources/icon-liquid-glass.png differ diff --git a/docs/branding-sources/liquid-glass-prompts.json b/docs/branding-sources/liquid-glass-prompts.json new file mode 100644 index 0000000..e75bf81 --- /dev/null +++ b/docs/branding-sources/liquid-glass-prompts.json @@ -0,0 +1,7 @@ +{ + "engine": "Codex built-in image generation", + "design_reference": "https://www.apple.com/newsroom/2025/06/apple-introduces-a-delightful-and-elegant-new-software-design/", + "direction": "Restrained Apple-inspired translucency, shallow depth, soft colors, thin edge highlights, and calm typography", + "icon": "Style-transfer correction: the user wants Apple's restrained Liquid Glass visual language. Use the supplied FLAT Functionalities icon as the exact shape reference, and create a minimal, quiet macOS-style app icon with a VERY SUBTLE thin translucent material. Preserve the exact four hollow diamond tiles (violet at top, teal at left, warm amber at right, blue below) and two blue V platform strokes, their spacing, centered layout, relative sizes, and front-facing silhouette. The glyph should still read as clean flat graphic geometry, with shallow layered depth and only tiny soft optical highlights along its edges. Colors are softened and slightly translucent, with quiet colored refraction, not bright jewel colors. Put the mark on a calm near-white gently translucent rounded-square tile. A single faint hairline white highlight follows the tile's perimeter; the tile has almost no visible thickness. Soft ambient light, tiny diffused shadow, neutral airy palette, ample breathing room, polished simplicity. Keep color in the small glyph; the tile itself is nearly colorless. Think Apple Liquid Glass: light transmitting through a thin optical surface, subtle edge bending, soft translucency and precise geometry. Do NOT make thick resin tubes, puffy gummy shapes, deep extrusion, chrome borders, faceted gems, concentric bevel rings, vivid gradient bands, specular streaks, glow, glitter, sparkles, neon colors, over-reflective plastic, heavy shadows, visual noise, decorative objects, or letters. Transparent outside the tile with clean smooth alpha corners. High resolution square PNG, legible at 128px. The finish should be understated enough that the material is noticed after the symbol, not before.", + "banner": "Revise this banner toward Apple's understated Liquid Glass aesthetic. Image 1 is the flat Dynamic Functionalities bento composition and exact wording reference. Image 2 is the revised restrained icon to use and match. Preserve the five-panel bento layout, the large left product panel, four right feature panels, all words, broad spacing, and extra-wide 1544x500 aspect ratio (3.088:1). Rebuild the material as THIN, almost colorless translucent UI surfaces: soft background blur passing through each panel, a single hairline white edge, tiny gentle edge refraction, nearly imperceptible depth, and a soft low-opacity shadow. It should look like a refined Apple macOS/iOS product design, not a 3D resin render. Make the entire composition LIGHT and airy. Replace the dark navy hero panel with pale pearl translucent glass, dark charcoal title text, and clean readable supporting copy. Use calm, soft typography: a clean medium/semibold modern sans-serif heading, not the heavy black letterforms of the original; retain clear product-name hierarchy. Use the revised icon from image 2 at the existing small brand-icon position. The four module glyphs remain crisp clean monoline symbols, colored mildly; no extruded glossy material on those symbols. Feature panels are mostly clear/white, with the faintest warm amber, sage/mint, lilac, and sky-blue tint filtering through the background. The pale backdrop may have a very gentle broad pastel wash that explains translucency, but no shapes, blobs, objects, noise, or strong gradients. Material effects must be subtle; content should be seen first. Exact text: 'WordPress toolkit'; 'Dynamic' then 'Functionalities'; 'Enable only what you need.'; '19 optional modules'; 'Free and open source'; 'Content Tools'; 'Duplicate into drafts'; 'Link Health'; 'Find broken links'; 'Fonts & styles'; 'Built for reuse'; 'Site Activity'; 'See what changed'. Keep text flat, sharp, front-facing and high contrast, with generous padding. No chrome, shiny tubes, colored rims, thick borders, double outlines, bright specular streaks, saturated blue hero card, deep bevels, chunky or puffy glass, neon glow, candy-like highlights, floating objects, confetti, droplets, glitter, fake UI, watermarks, gibberish, or cropped copy. Keep the outer edges and shadow soft and calm. Deliver a very wide horizontal banner, same aspect as image 1, clean and understated, ready for WordPress listing PNG sizes." +} diff --git a/docs/branding-sources/listing-manifest.json b/docs/branding-sources/listing-manifest.json new file mode 100644 index 0000000..6712703 --- /dev/null +++ b/docs/branding-sources/listing-manifest.json @@ -0,0 +1,38 @@ +[ + { + "path": ".wordpress-org/icon-128x128.png", + "pixels": [ + 128, + 128 + ], + "bytes": 26059, + "sha256": "5d60c276f788117dd298b06b8650ae28c9387c54f45766454ea8dfd960658113" + }, + { + "path": ".wordpress-org/icon-256x256.png", + "pixels": [ + 256, + 256 + ], + "bytes": 80309, + "sha256": "13348e9d8e731621427bf6a171858a016b9e069f1637d729295b6d0f386e575a" + }, + { + "path": ".wordpress-org/banner-772x250.png", + "pixels": [ + 772, + 250 + ], + "bytes": 196875, + "sha256": "6ea5f09e6b9995f96d4bfbb5130cf3f5370309f2208090df04948b3e3d10b805" + }, + { + "path": ".wordpress-org/banner-1544x500.png", + "pixels": [ + 1544, + 500 + ], + "bytes": 684132, + "sha256": "75f4cb2225b45a53f3cd2ec7c2e67f053eca36473a35aa964818198a7b79ef75" + } +] diff --git a/docs/link-health-live-verification.md b/docs/link-health-live-verification.md new file mode 100644 index 0000000..2dbe067 --- /dev/null +++ b/docs/link-health-live-verification.md @@ -0,0 +1,26 @@ +# Link Health live scanning and result pagination + +Gatilab's scan was advancing on its external cron schedule while the settings page displayed a static snapshot. The site had `DISABLE_WP_CRON` enabled, 1,161 eligible posts/pages, and only four link checks per scheduled batch. The report also paginated by one source post, producing short or empty-looking result pages. A separate stop-control regression rejected Stop while a worker held its lease. + +## Change + +- The open, visible admin workspace advances one bounded batch at a time through a nonce- and capability-protected endpoint. It monitors existing workers instead of overlapping them. Hidden tabs pause browser-driven work; the background schedule remains available. +- A live state label, activity indicator, progress bar, completed-post count, checked-link count, and refreshed results distinguish waiting, checking, stopping, stopped, completed, disabled, and error states. +- Stop is queued until the current bounded batch finishes. Resume preserves the run and cursor. Generation checks prevent a stale browser from advancing or stopping a different scan. Late client responses cannot overwrite Stop, and failed Stop requests do not restart browser-driven work. +- Results paginate across sources in groups of 50. A small per-post count index avoids deserializing every report on each refresh; older candidate reports are backfilled once. Empty reports consume no result rows. The index follows the existing opt-in uninstall cleanup. +- The results table uses the full module width and scrolls within its container on narrow screens. Native forms, CSV export, and manual one-batch fallback remain available. +- `readme.txt` now describes 1.7.0, live scanning, pagination, scan limits, optional scheduling, and the actual storage locations. Unsupported blanket performance/compatibility claims were tightened. + +## Verification + +- The stop-during-worker and stale-browser regressions failed on the unfixed code, then passed with the fix. +- PHP syntax, coding standards/PHP 7.4 compatibility, and 158 PHP tests / 844 assertions passed. Tests cover worker cancellation, expired cancellation, saved-cursor resume, private progress fields, admin/nonce rejection, and complete 50/50/20 pagination across 120 fixture results with private sources excluded. +- All 13 JavaScript tests passed, including five live-scanner tests for bounded work, result refresh, late-response cancellation, failed Stop handling, other workers, tab visibility, and expired authorization. +- Gatilab: the visible page advanced from 88 to 176 checked links without a reload. Stop settled at 180 links and 30 completed posts; its background event was cleared. Resume preserved progress and advanced to 380 links and 88 posts during the next observation. +- The second result page contained 50 rows, covered results 51–100, and had no overlap with the first page. The URL uses a single result-page parameter. +- At 390px, the page had no horizontal overflow, the back button was 44px high, and the wide table scrolled inside its own container. +- An anonymous request to the live endpoint was rejected with HTTP 400 and no handler execution. Administrator/nonce negative paths are also covered in PHP tests. +- All 147 installed runtime files matched the verified ZIP. The 19 module option arrays and all 1,203 existing post/page content, type, and status hashes matched the pre-deployment snapshot. Scan reports and their count metadata advanced as intended. +- The submenu alias loaded the live-scanner asset and the same 50-result layout. A real workspace screenshot was added to the WordPress.org assets and readme. + +Recovery files are retained in `/home/gatilab/functionalities-live-links-20261002`: the prior plugin and protected-data archives, settings/source-hash snapshot, candidate ZIP, and verification receipts. The original user-started scan and existing reports were retained throughout testing. diff --git a/docs/plans/2026-10-02-v1.7.0-modules-design.md b/docs/plans/2026-10-02-v1.7.0-modules-design.md new file mode 100644 index 0000000..b672364 --- /dev/null +++ b/docs/plans/2026-10-02-v1.7.0-modules-design.md @@ -0,0 +1,23 @@ +# Functionalities 1.7.0 modules + +Approved scope: Content Tools, Link Health, and Site Activity. Implementation and snapshot-backed Gatilab testing are authorized. Public release is a separate step. + +All modules remain disabled by default. They use the existing registry and module options; no new services, accounts, credentials, database tables, or frontend assets are required. Minimum WordPress 6.3 / PHP 7.4 remains unchanged. + +## Content Tools + +Posts/pages gain a permission-checked Duplicate as draft action. Preserve stored block content, excerpt, supported taxonomies, featured image, and page template. Use a developer metadata allowlist; exclude edit locks, history, plugin scan metadata, comments, revisions, source author, slug, password, and publication status. The current user owns the draft. Roll back the draft on taxonomy/metadata failure. Check edit_post, destination create_posts, taxonomy assignment, and the action nonce. + +## Link Health + +Scan anchor URLs in stored public, published posts/pages, excluding password-protected content. Dynamic blocks, shortcodes, navigation, and rendered-only links are outside this version's coverage. Use the core HTML tag processor without rewriting content. Resolve relative links against the source permalink, deduplicate normalized URLs, reject credentials/private hosts and unsupported protocols, and use safe HEAD requests with bounded GET confirmation/fallback. 404/410 are broken only after GET confirmation; auth errors, throttling, transport failures, and 5xx are inconclusive. + +Keep per-post results in postmeta, deduplicated URL checks in expiring transients, and a protected JSON scan cursor with an expiring worker lease. A bounded batch resumes inside a large post, uses an ID cursor, and is safe against overlapping workers. Each post is limited to 1,000 unique URLs and reports truncation explicitly. Report pages contain one source post and at most 50 visible links; CSV export streams every source report. A content hash prevents publishing stale results after an edit. Manual start/resume, ignore/recheck per result, paginated reports, CSV export, and opt-in weekly scans are admin-only. Cron is visit-driven; manual resume is available. Retain results on disable; clear scheduled work. Uninstall honors the existing deletion preference. + +## Site Activity + +Log module-setting field names, plugin/theme activation/update, and post/page status transitions. Store actor ID, event, target, timestamp, and allowlisted status/field details only. Never store secrets, code bodies, full content, request data, visitor IPs, or authentication attempts. Protected atomic JSON storage retains at most 1,000 entries / 30 days. Include filtering/search, pagination, explicit clear, daily pruning, and user-erasure/deletion anonymization. Failure to log must not block the underlying operation. + +## Verification and rollout + +Each module is independently usable. Test positive and negative permissions, disabled loading, failures, bounded scans, safe HTTP redirects/private URLs, duplicate cache reuse, content-edit races, leases/resume, CSV escaping, retention, concurrent storage writes, privacy, import sanitization, and uninstall. Run composer check, npm test, real-core HTML API tests, translation freshness, and package validation. Snapshot Gatilab plugin/settings/content, install the verified candidate, exercise actions with disposable content, verify readback and public storage guards, then remove fixtures and restore module settings. Retain a plugin rollback archive and a verification report. diff --git a/docs/ui-polish-verification.md b/docs/ui-polish-verification.md new file mode 100644 index 0000000..87723dd --- /dev/null +++ b/docs/ui-polish-verification.md @@ -0,0 +1,23 @@ +# Backend UI and branding polish for 1.7.0 + +The original branding pass bundled the main-site media item, [Dynamic Functionalities icon](https://r2.gauravtiwari.org/wp-content/uploads/2026/09/functionalities-icon-wporg-20260927.svg), matching the WordPress.org SVG master byte for byte. The subsequent [sidebar and navigation pass](ui-sidebar-verification.md) uses a simplified color-adaptive vector in the admin menu and dashboard. Individual module pages retain only their module glyph. + +Backend module/action glyphs use a 53-icon subset from `~/Icons/svg/outline` (Tabler). The MIT license is bundled; paths and mappings are recorded in `admin-icon-sources.json`. Controls use WordPress's selected admin accent. Unsupported third-party Dashicons retain their native rendering. + +Text/URL/number fields and selects use shared sizing, spacing, borders, focus states, and label alignment: 40px on desktop and 44px at phone width. Legacy component/font groups gain associated labels, including dynamically added groups. PWA repeaters have native visible labels and equal-width columns. Font upload rows reflow on phones. The activity search field is explicitly a search control. Fourteen older metadata/threshold fields gain clear accessible labels, and the legacy redirect selector includes the already-supported 308 status. + +The sidebar and dashboard/alias routes load the same assets, including PWA controls. Backend-only styles remain scoped; product settings, option names, public output, and module enablement are unchanged. Edited backend styles/scripts use a file timestamp in their cache key for candidate testing. + +## Verification + +- PHP lint, WordPress coding standards/PHP compatibility, and all 149 PHP tests passed. +- All eight JavaScript tests and syntax checks passed. +- Translation regeneration/freshness and distribution packaging passed. +- All 53 local SVGs parsed, and the editable banner passed strict SVG validation. +- The banner and icon were rendered and visually inspected at their WordPress.org delivery sizes. Banners now describe 19 optional modules. Directory screenshots were refreshed from the real backend, with the surrounding account navigation cropped out. +- Gatilab (WordPress 7.1.2/PHP 8.5.10): all 19 screens were inspected at desktop width. No page overflow or unlabelled visible value controls remained after initialization. +- Seven dense screens plus the dashboard were checked at 390px. Controls reflowed without page overflow. Expanded fonts and dynamically added PWA rows had aligned, labelled controls and 44px phone targets. +- Existing font/component edit panels were opened for inspection; no settings form was saved. Dynamically added PWA rows were discarded by navigation. +- The 19 module option arrays and homepage content/metadata matched the pre-polish snapshot (21 checks). + +The plugin and snapshot recovery files are retained on Gatilab in `/home/gatilab/functionalities-ui-20261002`. The public 1.7.0 release and WordPress.org asset deployment remain separate from this candidate. diff --git a/docs/ui-sidebar-verification.md b/docs/ui-sidebar-verification.md new file mode 100644 index 0000000..87ae091 --- /dev/null +++ b/docs/ui-sidebar-verification.md @@ -0,0 +1,21 @@ +# Module navigation, SVG mark, and settings sidebar + +The backend mark is an original monoline simplification of the official four-module platform graphic. `assets/brand/functionalities-admin.svg` contains editable vector paths, with no embedded raster image. CSS masks inherit WordPress menu colors, including hover/current states. The dashboard uses the same geometry in its admin accent color. The [colorful listing icon and bento banner](branding-assets.md) reuse this silhouette for WordPress.org. + +Every module header includes a Back to modules button and a semantic breadcrumb with the current module. Task Manager also retains project ancestry. Individual settings pages show their module glyph without repeating the product mark. The navigation arrow comes from `~/Icons/svg/outline/arrow-left.svg`; the bundled Tabler subset now contains 54 icons with its MIT license. + +The shared PHP renderer collects documentation separately from the complete settings form. Informational guidance is placed in a right sidebar at 1180px and above; it stacks after settings on narrower screens. Native form ownership, hidden fields, nonce fields, and submission targets are preserved. Active warnings, scan status, recent lockout actions, and result controls remain in the main area. Tables scroll within their own container when necessary. Container queries stack dense form rows when the main column becomes narrow. + +## Verification on Gatilab + +- WordPress 7.1.2 / PHP 8.5.10, with the 1.7.0 candidate active. +- All 19 module pages: current breadcrumb item and back button present; no product mark in individual headers; no nested forms, orphan named controls, sidebar controls, or page overflow. +- Link Health's direct submenu alias also includes its current breadcrumb. Back to modules was clicked and returned to all 19 dashboard cards. +- Schema, Link Health, Site Activity, Components, Fonts, SVG Icons, and Header & Footer were checked at 1280px, 1024px, and 390px. The guide appeared on the right at 1280px and below content at 1024px/390px. Visible value controls were at least 40px on desktop and 44px on phones; the phone back button was 44px high. +- Schema's 11 value/checkbox controls matched their pre-deployment values and names. +- The SVG was rendered at 22px and 64px on light/dark surfaces and passed strict validation. Actual WordPress Light and Midnight menu previews confirmed inherited icon colors and visible contrast. Profile changes were discarded; the original Default scheme was restored. +- Three layout regression tests verify guidance outside the complete form, warnings remaining in the form, no empty sidebar, standalone rendering, and buffer/state restoration after exceptions. The layout is server-rendered and does not require JavaScript. +- PHP lint, WordPress coding standards/PHP 7.4 compatibility, and 152 PHP tests / 810 assertions passed. All eight JavaScript tests passed. Translation freshness and distribution checks passed. +- All 19 module option arrays and homepage content/metadata matched the snapshot, for 21 unchanged-state checks. Installed runtime hashes matched the verified ZIP's 146 files. + +Recovery files are retained on Gatilab in `/home/gatilab/functionalities-sidebar-20261002`. Screenshots and JSON receipts are saved with the local UI verification artifacts. The public release and WordPress.org asset deployment remain separate from this candidate. diff --git a/docs/v1.7.0-verification.md b/docs/v1.7.0-verification.md new file mode 100644 index 0000000..635253b --- /dev/null +++ b/docs/v1.7.0-verification.md @@ -0,0 +1,39 @@ +# Functionalities 1.7.0 verification + +The candidate adds Content Tools, Link Health, and Site Activity, bringing the registry to 19 independently enabled modules. Public release is pending. + +## Automated checks + +- `FUNCTIONALITIES_WP_DIR=/tmp/functionalities-wp63-audit-20261001/src composer check`: PHP syntax, WordPress coding standards/PHP 7.4 compatibility, and 149 PHPUnit tests / 798 assertions passed. +- The same suite passed with a WordPress 6.9 core checkout on local PHP 8.5. WordPress's actual HTML and URL APIs were loaded in both suites. +- `npm test`: all eight JavaScript tests passed. The existing WordPress 7 bundle source was unchanged. +- Translation regeneration and `bin/make-pot.sh --check` passed. +- `build.sh` produced an 84-file runtime ZIP. Tests, docs, source, Composer dependencies, and Node dependencies are excluded; the required bundled Prism assets remain included. +- New regressions cover permissions, native protected metadata, rollback, safe HTTP fallback/redirects, cache reuse, private targets, cursor resume, stale content/slug changes, competing/expired leases, retained history, anonymization, upgrade failures, import allowlists, CSV formula escaping, disabled loading, and per-site uninstall retention. + +CI results are available on the pull request's Checks tab. This document records the local and live observations independently of CI. + +## Gatilab checks + +Target: `https://gatilab.com`, WordPress 7.1.2 / PHP 8.5.10. + +The server workflow passed post duplication with byte-preserved Gutenberg content, owner/draft status, taxonomy and featured-image copying, exclusion of identity/scan metadata, negative permissions, password-protected scan exclusion, bounded/deduplicated scan completion, real HTTP 200/404/redirects, private-IP rejection, ignore/recheck, arbitrary-target rejection, activity redaction, publishing events, privacy export/anonymization, and anonymous storage protection. Four additional native page-copy checks passed, including the page template. + +The browser verified all three settings forms, the duplicate row action and editor navigation, scan start/resume, ignore/recheck, CSV download, activity search/event filtering, and the restored dashboard. The CSV contained exactly the expected OK/broken/unsafe fixture results. Layout was observed at natural browser widths of 684 and 1,280 pixels, without page overflow. A stale Cloudflare stylesheet was purged by its exact URL using the [single-file purge API](https://developers.cloudflare.com/api/resources/cache/methods/purge/); the response changed to MISS with the final CSS, and computed action-form padding was verified as 0px. Browser cache bypass was restored after verification. + +The activity-clear route was exercised with WordPress's real capability and nonce checks: anonymous access, an invalid nonce, and omitted confirmation were rejected without deleting history; a confirmed administrator request cleared the test history. The weekly event was created on opt-in and removed on opt-out. + +A repeat server pass on the final candidate succeeded after adding the shared report-commit lock. Network requests are outside that lock. Failed/expired workers cannot overwrite newer reports, and rechecks merge their one result rather than replacing another worker's whole report. + +## Restored state and recovery + +- Disposable posts/pages/drafts and their revisions were removed. +- All original module settings were preserved; the three new options were restored to their original absent/default-disabled state. +- Test histories, scan state, URL cache entries, and scheduled work were cleared. No visitor-facing module was newly enabled. +- Homepage content and metadata hashes matched the pre-install snapshot. The public homepage remained available. +- Installed runtime file hashes were compared with the extracted ZIP manifest. The final manifest is recorded with the verification artifacts. +- The installed pre-candidate readme differed from the tagged source only in older release wording; the original remains in the backup. Runtime PHP/JS/CSS files matched 1.6.3 before deployment. + +Recovery on the Gatilab host is retained in `/home/gatilab/functionalities-170-20261002`: original plugin archive, original protected data archive, a private settings snapshot, candidate ZIPs, and verification receipts. Restore the archived plugin first if rollback is needed; restore only scoped settings/data after comparing current state. + +The existing Fonts/Components/SVG editor-iframe visual release checklist remains a separate release gate; this change's editor test confirmed native duplicate navigation and stored block content, rather than a new iframe styling change. diff --git a/functionalities.php b/functionalities.php index 957e7f6..618dde1 100644 --- a/functionalities.php +++ b/functionalities.php @@ -2,8 +2,8 @@ /** * Plugin Name: Dynamic Functionalities * Plugin URI: https://functionalities.dev - * Description: All-in-one WordPress optimization toolkit. 15+ modules for performance, security, SEO, and content management. - * Version: 1.6.3 + * Description: All-in-one WordPress optimization toolkit. 19 modules for performance, security, SEO, and content management. + * Version: 1.7.0 * Author: Gaurav Tiwari * Author URI: https://gauravtiwari.org * License: GPL-2.0-or-later @@ -19,7 +19,7 @@ // Define constants. if ( ! defined( 'FUNCTIONALITIES_VERSION' ) ) { - define( 'FUNCTIONALITIES_VERSION', '1.6.3' ); + define( 'FUNCTIONALITIES_VERSION', '1.7.0' ); } if ( ! defined( 'FUNCTIONALITIES_FILE' ) ) { define( 'FUNCTIONALITIES_FILE', __FILE__ ); @@ -59,6 +59,12 @@ function ( string $class ) { } ); +// Retained activity history remains erasable/exportable even with its module off. +\add_action( 'deleted_user', array( '\\Functionalities\\Features\\Site_Activity', 'anonymize_user' ) ); +\add_action( 'wpmu_delete_user', array( '\\Functionalities\\Features\\Site_Activity', 'anonymize_network_user' ) ); +\add_filter( 'wp_privacy_personal_data_erasers', array( '\\Functionalities\\Features\\Site_Activity', 'register_eraser' ) ); +\add_filter( 'wp_privacy_personal_data_exporters', array( '\\Functionalities\\Features\\Site_Activity', 'register_exporter' ) ); + // Initialize admin and only the modules required for this request. \add_action( 'plugins_loaded', @@ -119,6 +125,9 @@ function ( array $links, string $file ): array { function () { \wp_clear_scheduled_hook( 'functionalities_assumption_background_scan' ); \wp_clear_scheduled_hook( 'functionalities_redirect_flush_buffer' ); + \wp_clear_scheduled_hook( 'functionalities_link_health_batch' ); + \wp_clear_scheduled_hook( 'functionalities_link_health_weekly' ); + \wp_clear_scheduled_hook( 'functionalities_activity_prune' ); if ( function_exists( 'flush_rewrite_rules' ) ) { \flush_rewrite_rules(); } diff --git a/includes/admin/class-admin-icons.php b/includes/admin/class-admin-icons.php new file mode 100644 index 0000000..594fe02 --- /dev/null +++ b/includes/admin/class-admin-icons.php @@ -0,0 +1,45 @@ + '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', + ); + /** Return a decorative module glyph with an allowlisted CSS class. */ + public static function module( string $slug ): string { + $name = self::MODULES[ $slug ] ?? 'settings'; + return ''; + } + /** Return a color-adaptive vector mark; nearby text supplies its accessible name. */ + public static function brand( int $size = 48 ): string { + $size = max( 20, min( 96, $size ) ); + return ''; + } +} diff --git a/includes/admin/class-admin-ui.php b/includes/admin/class-admin-ui.php index 84fa2b7..cebaadc 100644 --- a/includes/admin/class-admin-ui.php +++ b/includes/admin/class-admin-ui.php @@ -17,6 +17,77 @@ * Admin UI helper class. */ class Admin_UI { + /** + * Guidance collected while a module's settings render. + * + * @var array|null + */ + private static $guidance = null; + + /** Render settings and guidance separately, keeping every form intact. */ + public static function render_settings_layout( callable $render ): void { + $previous = self::$guidance; + self::$guidance = array(); + $buffer_level = ob_get_level(); + ob_start(); + try { + $render(); + $content = ob_get_clean(); + $guidance = implode( '', self::$guidance ); + } finally { + while ( ob_get_level() > $buffer_level ) { + ob_end_clean(); + } + self::$guidance = $previous; + } + echo '
'; + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Captured, pre-escaped admin render output. + echo $content; + echo '
'; + if ( '' !== $guidance ) { + echo ''; + } + echo '
'; + } + + /** Render consistent module navigation, including optional project ancestry. */ + public static function render_navigation( string $title, string $slug, string $child = '' ): void { + echo ''; + } + + /** Render the shared product header without changing a module's form controls. */ + public static function render_header( string $title, string $description = '', string $slug = '', string $child = '' ): void { + if ( '' !== $slug ) { + self::render_navigation( $title, $slug, $child ); + } + echo '
'; + if ( '' === $slug ) { + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Trusted local asset markup from Admin_Icons. + echo Admin_Icons::brand( 64 ); + } + echo '

'; + if ( '' !== $slug ) { + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Allowlisted local icon markup. + echo Admin_Icons::module( $slug ) . ' '; + } + echo \esc_html( $title ) . ' v' . \esc_html( FUNCTIONALITIES_VERSION ) . '

'; + if ( '' !== $description ) { + echo '

' . \esc_html( $description ) . '

'; + } + echo '
'; + } + /** * Render a documentation section with details/summary accordion. @@ -31,11 +102,13 @@ public static function render_docs_section( string $title, string $content, stri $open_attr = $open ? ' open' : ''; $class = 'functionalities-docs-accordion functionalities-docs-' . esc_attr( $type ); - echo '
'; - echo '' . esc_html( $title ) . ''; - // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Content is pre-escaped HTML from render functions. - echo '
' . $content . '
'; - echo '
'; + $html = '
' . esc_html( $title ) . '
' . $content . '
'; + if ( null !== self::$guidance ) { + self::$guidance[] = $html; + } else { + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Content is pre-escaped HTML from render functions. + echo $html; + } } /** @@ -54,7 +127,8 @@ public static function render_features_docs( array $items ): void { self::render_docs_section( \__( 'What This Module Does', 'functionalities' ), $content, - 'info' + 'info', + true ); } @@ -68,7 +142,8 @@ public static function render_usage_docs( string $description ): void { self::render_docs_section( \__( 'How to Use', 'functionalities' ), '

' . 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 '
'; + if ( null === self::$guidance ) { + echo '
'; + } if ( ! empty( $config['features'] ) ) { self::render_features_docs( $config['features'] ); @@ -132,6 +209,8 @@ public static function render_module_docs( array $config ): void { self::render_developer_docs( $config['hooks'] ); } - echo '
'; + if ( null === self::$guidance ) { + echo '
'; + } } } diff --git a/includes/admin/class-link-health-controller.php b/includes/admin/class-link-health-controller.php new file mode 100644 index 0000000..31db7f8 --- /dev/null +++ b/includes/admin/class-link-health-controller.php @@ -0,0 +1,117 @@ + \admin_url( 'admin-ajax.php' ), + 'nonce' => \wp_create_nonce( self::NONCE ), + 'checking' => \__( 'Checking links…', 'functionalities' ), + 'stopping' => \__( 'Finishing the current batch…', 'functionalities' ), + 'retry' => \__( 'Retry scan', 'functionalities' ), + 'resume' => \__( 'Resume scan', 'functionalities' ), + 'connectionError' => \__( 'Live updates were interrupted. Reconnecting… Your background scan can continue.', 'functionalities' ), + 'requestError' => \__( 'The request failed. Refresh this page and try again.', 'functionalities' ), + ) + ); + } + + /** A translated, non-sensitive snapshot for both initial HTML and live updates. */ + public static function progress(): array { + $data = Link_Health::progress(); + $labels = array( + 'idle' => \__( 'Not started', 'functionalities' ), + 'checking' => \__( 'Checking links…', 'functionalities' ), + 'waiting' => \__( 'Waiting for the next batch', 'functionalities' ), + 'stopping' => \__( 'Finishing the current batch…', 'functionalities' ), + 'stopped' => \__( 'Stopped', 'functionalities' ), + 'completed' => \__( 'Scan completed', 'functionalities' ), + 'disabled' => \__( 'Link Health is disabled', 'functionalities' ), + 'error' => \__( 'Scan needs attention. Check private storage and retry.', 'functionalities' ), + ); + $data['label'] = $labels[ $data['phase'] ] ?? $labels['idle']; + /* translators: 1: completed posts, 2: total posts, 3: processed links. */ + $data['summary'] = sprintf( \__( '%1$d of %2$d posts completed · %3$d links checked', 'functionalities' ), $data['posts'], $data['total'], $data['links'] ); + return $data; + } + + /** Each mutation requires an admin, a nonce, an enabled module, and the current run. */ + public static function ajax(): void { + if ( ! \current_user_can( 'manage_options' ) || ! \check_ajax_referer( self::NONCE, 'nonce', false ) ) { + \wp_send_json_error( array( 'message' => \__( 'Refresh this page and sign in as an administrator.', 'functionalities' ) ), 403 ); + return; + } + $operation = \sanitize_key( \wp_unslash( $_POST['operation'] ?? 'status' ) ); + $run = \sanitize_text_field( \wp_unslash( $_POST['run'] ?? '' ) ); + if ( ! in_array( $operation, array( 'status', 'start', 'batch', 'resume', 'stop', 'recheck', 'ignore' ), true ) ) { + \wp_send_json_error( array( 'message' => \__( 'Unknown action.', 'functionalities' ) ), 400 ); + return; + } + if ( 'status' !== $operation && ! \Functionalities\Core\Module_Registry::is_enabled( 'link-health' ) ) { + \wp_send_json_error( array( 'message' => \__( 'Enable Link Health before running a scan.', 'functionalities' ) ), 403 ); + return; + } + if ( in_array( $operation, array( 'batch', 'resume', 'stop' ), true ) && '' === $run ) { + \wp_send_json_error( array( 'message' => \__( 'Refresh the scan status before continuing.', 'functionalities' ) ), 400 ); + return; + } + $result = true; + switch ( $operation ) { + case 'start': + $result = Link_Health::start_scan(); + break; + case 'resume': + $result = 'stopped' === ( Link_Health::state()['status'] ?? '' ) ? Link_Health::resume_scan( $run ) : Link_Health::run_batch( $run ); + break; + case 'batch': + $result = Link_Health::run_batch( $run ); + break; + case 'stop': + $result = Link_Health::stop_scan( $run ); + break; + case 'recheck': + case 'ignore': + $post_id = \absint( $_POST['post_id'] ?? 0 ); + $url = \sanitize_text_field( \wp_unslash( $_POST['url'] ?? '' ) ); + $result = 'recheck' === $operation ? Link_Health::recheck( $post_id, $url ) : Link_Health::set_ignored( $post_id, $url, true ); + break; + } + if ( \is_wp_error( $result ) && ! ( 'batch' === $operation && 'update_aborted' === $result->get_error_code() ) ) { + \wp_send_json_error( array( 'message' => $result->get_error_message() ), 409 ); + return; + } + $data = array( 'progress' => self::progress() ); + if ( ! empty( $_POST['report'] ) && \Functionalities\Core\Module_Registry::is_enabled( 'link-health' ) ) { + ob_start(); + Module_Controller::render_link_health_report( max( 1, \absint( $_POST['report_page'] ?? 1 ) ) ); + $data['html'] = ob_get_clean(); + } + \wp_send_json_success( $data ); + } + + public static function render( array $module ): void { + Module_Controller::render_module_link_health( $module ); + } +} diff --git a/includes/admin/class-module-controller.php b/includes/admin/class-module-controller.php index 2c5c147..2d40264 100644 --- a/includes/admin/class-module-controller.php +++ b/includes/admin/class-module-controller.php @@ -12,6 +12,7 @@ } // Load traits. +require_once __DIR__ . '/trait-admin-utilities-ui.php'; require_once __DIR__ . '/trait-admin-ajax.php'; require_once __DIR__ . '/trait-admin-options.php'; require_once __DIR__ . '/trait-admin-sanitizers.php'; @@ -34,6 +35,7 @@ */ class Module_Controller { + use Admin_Utilities_UI; use Admin_Ajax; use Admin_Options; use Admin_Sanitizers; @@ -60,6 +62,7 @@ class Module_Controller { */ public static function init(): void { self::define_modules(); + self::init_utility_actions(); \add_action( 'admin_menu', array( __CLASS__, 'register_menu' ) ); \add_action( 'admin_init', array( __CLASS__, 'register_settings' ) ); \add_action( 'admin_enqueue_scripts', array( __CLASS__, 'enqueue_admin_assets' ) ); @@ -99,7 +102,7 @@ public static function register_menu(): void { 'manage_options', $parent_slug, array( __CLASS__, 'render_main_page' ), - 'dashicons-admin-generic', + 'none', 65 ); @@ -139,6 +142,7 @@ public static function register_menu(): void { * @return void */ public static function enqueue_admin_assets( $hook ): void { + \wp_enqueue_style( 'functionalities-admin-brand', FUNCTIONALITIES_URL . 'assets/css/admin-brand.css', array(), self::admin_asset_version( 'assets/css/admin-brand.css' ) ); if ( strpos( $hook, 'functionalities' ) === false ) { return; } @@ -148,6 +152,9 @@ public static function enqueue_admin_assets( $hook ): void { $page = isset( $_GET['page'] ) ? \sanitize_key( $_GET['page'] ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Module detection doesn't require nonce. $module = isset( $_GET['module'] ) ? \sanitize_key( $_GET['module'] ) : ''; + if ( '' === $module && 0 === strpos( $page, 'functionalities-' ) ) { + $module = substr( $page, strlen( 'functionalities-' ) ); + } if ( 'task-manager' === $module || 'functionalities-task-manager' === $page ) { $deps[] = 'jquery-ui-sortable'; @@ -178,6 +185,9 @@ public static function enqueue_admin_assets( $hook ): void { \wp_enqueue_style( 'wp-color-picker' ); $deps[] = 'wp-color-picker'; } + if ( 'link-health' === $module ) { + Link_Health_Controller::enqueue(); + } \wp_enqueue_style( 'functionalities-admin', @@ -186,6 +196,10 @@ public static function enqueue_admin_assets( $hook ): void { FUNCTIONALITIES_VERSION ); + \wp_enqueue_style( 'functionalities-admin-icons', FUNCTIONALITIES_URL . 'assets/css/admin-icons.css', array( 'functionalities-admin' ), self::admin_asset_version( 'assets/css/admin-icons.css' ) ); + \wp_enqueue_style( 'functionalities-admin-polish', FUNCTIONALITIES_URL . 'assets/css/admin-polish.css', array( 'functionalities-admin-icons' ), self::admin_asset_version( 'assets/css/admin-polish.css' ) ); + \wp_enqueue_script( 'functionalities-admin-polish', FUNCTIONALITIES_URL . 'assets/js/admin-polish.js', array(), self::admin_asset_version( 'assets/js/admin-polish.js' ), true ); + \wp_enqueue_script( 'functionalities-admin', FUNCTIONALITIES_URL . 'assets/js/admin.js', @@ -207,6 +221,12 @@ public static function enqueue_admin_assets( $hook ): void { ); } + /** Give edited backend assets a fresh cache key while a release is being tested. */ + private static function admin_asset_version( string $relative ): string { + $path = FUNCTIONALITIES_DIR . $relative; + return FUNCTIONALITIES_VERSION . '-' . ( is_file( $path ) ? (string) filemtime( $path ) : '0' ); + } + /** * Render main admin page with module navigation. * @@ -255,10 +275,7 @@ private static function is_module_enabled( string $slug ): bool { private static function render_dashboard(): void { ?>
-

-

- -

+
- +

-
+
- + - +
@@ -372,27 +389,21 @@ private static function render_module( $module_slug ): void { ?>
-

- - -

- - - -
- -
+ + + + '; + \settings_fields( $settings_group ); + \do_settings_sections( $settings_group ); + \submit_button(); + echo ''; + } + ); + ?>
- + diff --git a/includes/admin/class-module-docs.php b/includes/admin/class-module-docs.php index a98b720..f4956e8 100644 --- a/includes/admin/class-module-docs.php +++ b/includes/admin/class-module-docs.php @@ -36,6 +36,21 @@ public static function get( string $module ): array { */ public static function get_all(): array { return array( + 'content-tools' => array( + 'features' => array( \__( 'Duplicate posts/pages into an editable draft owned by the current user', 'functionalities' ), \__( 'Preserve block content, taxonomies, featured image, and page template', 'functionalities' ) ), + 'hooks' => array( + array( + 'name' => 'functionalities_content_tools_meta_keys', + 'description' => \__( 'Allowlist additional metadata keys to copy; edit locks and scan metadata remain excluded', 'functionalities' ), + ), + ), + ), + 'link-health' => array( + 'features' => array( \__( 'Resumable checks of stored public post/page links with cached URL results', 'functionalities' ), \__( 'Manual/weekly scans, ignore/recheck actions, and CSV export', 'functionalities' ), \__( 'Safe HTTP requests; confirmed missing pages are separated from inconclusive failures', 'functionalities' ) ), + ), + 'site-activity' => array( + 'features' => array( \__( 'Private history of module settings, publishing status, and plugin/theme changes', 'functionalities' ), \__( 'Searchable history bounded to 1,000 events and 30 days', 'functionalities' ), \__( 'Stores field names rather than values, with privacy export and anonymization', 'functionalities' ) ), + ), 'link-management' => array( 'features' => array( \__( 'Automatically adds rel="nofollow" to external links in post content', 'functionalities' ), diff --git a/includes/admin/class-settings-portability-controller.php b/includes/admin/class-settings-portability-controller.php index 8a6e1b6..1ca0a6d 100644 --- a/includes/admin/class-settings-portability-controller.php +++ b/includes/admin/class-settings-portability-controller.php @@ -411,6 +411,9 @@ private static function normalize_legacy_settings( string $slug, array $value ): */ private static function validate_module( string $slug, array $value, bool $include_code ): array { $sanitizers = array( + 'content-tools' => 'sanitize_content_tools', + 'link-health' => 'sanitize_link_health', + 'site-activity' => 'sanitize_site_activity', 'link-management' => 'sanitize_link_management', 'block-cleanup' => 'sanitize_block_cleanup', 'editor-links' => 'sanitize_editor_links', diff --git a/includes/admin/class-site-activity-controller.php b/includes/admin/class-site-activity-controller.php new file mode 100644 index 0000000..af5c927 --- /dev/null +++ b/includes/admin/class-site-activity-controller.php @@ -0,0 +1,17 @@ + false ), (array) \get_option( 'functionalities_content_tools', array() ) ); + } + public static function get_link_health_options(): array { + return array_merge( + array( + 'enabled' => false, + 'weekly_scan' => false, + ), + (array) \get_option( 'functionalities_link_health', array() ) + ); + } + public static function get_site_activity_options(): array { + return array_merge( array( 'enabled' => false ), (array) \get_option( 'functionalities_site_activity', array() ) ); + } + /** * Get link management options with defaults. diff --git a/includes/admin/trait-admin-pwa-ui.php b/includes/admin/trait-admin-pwa-ui.php index f06ba05..378297b 100644 --- a/includes/admin/trait-admin-pwa-ui.php +++ b/includes/admin/trait-admin-pwa-ui.php @@ -62,11 +62,11 @@ private static function render_shortcut_row( int $index, array $data ): void { $icon = \esc_attr( $data['icon'] ?? '' ); $prefix = 'functionalities_pwa[shortcuts][' . $index . ']'; echo '
'; - echo '
'; - echo ''; - echo ''; - echo ''; - echo ''; + echo '
'; + echo ''; + echo ''; + echo ''; + echo ''; echo '
'; echo ''; echo '
'; @@ -131,14 +131,14 @@ private static function render_screenshot_row( int $index, array $data ): void { $form = $data['form_factor'] ?? 'wide'; $prefix = 'functionalities_pwa[screenshots][' . $index . ']'; echo '
'; - echo '
'; - echo ''; - echo ''; - echo ''; - echo ''; + echo ''; + echo ''; + echo ''; echo '
'; echo ''; echo '
'; diff --git a/includes/admin/trait-admin-redirect-manager-ui.php b/includes/admin/trait-admin-redirect-manager-ui.php index 0c3d30c..df26a84 100644 --- a/includes/admin/trait-admin-redirect-manager-ui.php +++ b/includes/admin/trait-admin-redirect-manager-ui.php @@ -43,18 +43,8 @@ public static function render_module_redirect_manager( array $module ): void { $not_found = \Functionalities\Features\Redirect_Manager::get_404_log(); ?>
-

- - -

+ -
@@ -116,6 +106,7 @@ public static function render_module_redirect_manager( array $module ): void { + @@ -127,7 +118,7 @@ public static function render_module_redirect_manager( array $module ): void {
- +
diff --git a/includes/admin/trait-admin-sanitizers.php b/includes/admin/trait-admin-sanitizers.php index 15b2e10..a2e9a43 100644 --- a/includes/admin/trait-admin-sanitizers.php +++ b/includes/admin/trait-admin-sanitizers.php @@ -16,6 +16,19 @@ * Trait for sanitizer methods. */ trait Admin_Sanitizers { + public static function sanitize_content_tools( $input ): array { + return array( 'enabled' => ! empty( $input['enabled'] ) ); + } + public static function sanitize_link_health( $input ): array { + return array( + 'enabled' => ! empty( $input['enabled'] ), + 'weekly_scan' => ! empty( $input['weekly_scan'] ), + ); + } + public static function sanitize_site_activity( $input ): array { + return array( 'enabled' => ! empty( $input['enabled'] ) ); + } + /** * Sanitize link management settings. diff --git a/includes/admin/trait-admin-settings.php b/includes/admin/trait-admin-settings.php index b1bd2ae..76f8b26 100644 --- a/includes/admin/trait-admin-settings.php +++ b/includes/admin/trait-admin-settings.php @@ -15,6 +15,7 @@ trait Admin_Settings { public static function register_settings(): void { + self::register_utility_settings(); \register_setting( 'functionalities_link_management', 'functionalities_link_management', @@ -320,27 +321,24 @@ function () { function () { echo '

' . \esc_html__( 'Control which post types appear in the block editor link search suggestions.', 'functionalities' ) . '

'; - echo '
'; - echo '

' . \esc_html__( 'What This Module Does', 'functionalities' ) . '

'; - echo '
    '; + ob_start(); + echo '
      '; echo '
    • ' . \esc_html__( 'Limits link search results to specific post types when inserting links in Gutenberg', 'functionalities' ) . '
    • '; echo '
    • ' . \esc_html__( 'Reduces clutter by hiding unwanted content types from search results', 'functionalities' ) . '
    • '; echo '
    • ' . \esc_html__( 'Works with posts, pages, and custom post types', 'functionalities' ) . '
    • '; echo '
    '; - echo '
'; + Admin_UI::render_docs_section( \__( 'What This Module Does', 'functionalities' ), (string) ob_get_clean(), 'info', true ); - echo '
'; - echo '

' . \esc_html__( 'How to Use', 'functionalities' ) . '

'; - echo '

' . \esc_html__( 'Enable limitation, then check only the post types you want to appear when searching for links in the editor. Unchecked post types will be hidden from link search results.', 'functionalities' ) . '

'; - echo '
'; + ob_start(); + echo '

' . \esc_html__( 'Enable limitation, then check only the post types you want to appear when searching for links in the editor. Unchecked post types will be hidden from link search results.', 'functionalities' ) . '

'; + Admin_UI::render_docs_section( \__( 'How to Use', 'functionalities' ), (string) ob_get_clean(), 'usage', true ); - echo '
'; - echo '

' . \esc_html__( 'For Developers', 'functionalities' ) . '

'; - echo '

'; + ob_start(); + echo '

'; echo \esc_html__( 'Filter:', 'functionalities' ) . ' functionalities_editor_links_enabled — ' . \esc_html__( 'toggle feature', 'functionalities' ) . '
'; echo \esc_html__( 'Filter:', 'functionalities' ) . ' functionalities_editor_links_post_types — ' . \esc_html__( 'modify allowed post types', 'functionalities' ); echo '

'; - echo '
'; + Admin_UI::render_docs_section( \__( 'For Developers', 'functionalities' ), (string) ob_get_clean(), 'developer', false ); }, 'functionalities_editor_links' ); @@ -396,33 +394,30 @@ function () { function () { echo '

' . \esc_html__( 'Insert custom code snippets into your site header and footer without editing theme files.', 'functionalities' ) . '

'; - echo '
'; - echo '

' . \esc_html__( 'What This Module Does', 'functionalities' ) . '

'; - echo '
    '; + ob_start(); + echo '
      '; echo '
    • ' . \esc_html__( 'Native Google Analytics 4 integration - just enter your Measurement ID', 'functionalities' ) . '
    • '; echo '
    • ' . \esc_html__( 'Multiple snippets per location — each independently toggleable', 'functionalities' ) . '
    • '; echo '
    • ' . \esc_html__( 'Custom code for meta tags, scripts, styles, and tracking codes', 'functionalities' ) . '
    • '; echo '
    • ' . \esc_html__( 'Automatically skips admin pages, feeds, and REST API requests', 'functionalities' ) . '
    • '; echo '
    '; - echo '
'; + Admin_UI::render_docs_section( \__( 'What This Module Does', 'functionalities' ), (string) ob_get_clean(), 'info', true ); - echo '
'; - echo '

' . \esc_html__( 'Allowed Tags', 'functionalities' ) . '

'; - echo '

'; + ob_start(); + echo '

'; echo '<script>, <style>, <link>, <meta>, <noscript>'; echo '

'; - echo '
'; + Admin_UI::render_docs_section( \__( 'Allowed Tags', 'functionalities' ), (string) ob_get_clean(), 'usage', true ); - echo '
'; - echo '

' . \esc_html__( 'For Developers', 'functionalities' ) . '

'; - echo '

'; + ob_start(); + echo '

'; echo \esc_html__( 'Filter:', 'functionalities' ) . ' functionalities_snippets_output_enabled — ' . \esc_html__( 'disable on specific pages', 'functionalities' ) . '
'; echo \esc_html__( 'Filter:', 'functionalities' ) . ' functionalities_snippets_ga4_enabled — ' . \esc_html__( 'control GA4 per user/page', 'functionalities' ) . '
'; echo \esc_html__( 'Filter:', 'functionalities' ) . ' functionalities_snippets_header_code — ' . \esc_html__( 'modify header code', 'functionalities' ) . '
'; echo \esc_html__( 'Filter:', 'functionalities' ) . ' functionalities_snippets_footer_code — ' . \esc_html__( 'modify footer code', 'functionalities' ) . '
'; echo \esc_html__( 'Actions:', 'functionalities' ) . ' functionalities_before/after_header/footer_snippets'; echo '

'; - echo '
'; + Admin_UI::render_docs_section( \__( 'For Developers', 'functionalities' ), (string) ob_get_clean(), 'developer', false ); }, 'functionalities_snippets' ); @@ -457,7 +452,7 @@ function () { function () { $o = self::get_snippets_options(); $val = isset( $o['ga4_id'] ) ? (string) $o['ga4_id'] : ''; - echo ''; + echo ''; }, 'functionalities_snippets', 'functionalities_snippets_section' @@ -518,33 +513,30 @@ function () { function () { echo '

' . \esc_html__( 'Add Schema.org microdata attributes to improve search engine understanding of your content.', 'functionalities' ) . '

'; - echo '
'; - echo '

' . \esc_html__( 'What This Module Does', 'functionalities' ) . '

'; - echo '
    '; + ob_start(); + echo '
      '; echo '
    • ' . \esc_html__( 'Adds itemscope/itemtype to the HTML element for page-level schema', 'functionalities' ) . '
    • '; echo '
    • ' . \esc_html__( 'Wraps article content with Article/BlogPosting microdata', 'functionalities' ) . '
    • '; echo '
    • ' . \esc_html__( 'Adds structured data for headlines, dates, and authors', 'functionalities' ) . '
    • '; echo '
    • ' . \esc_html__( 'Marks header and footer regions with WPHeader/WPFooter types', 'functionalities' ) . '
    • '; echo '
    '; - echo '
'; + Admin_UI::render_docs_section( \__( 'What This Module Does', 'functionalities' ), (string) ob_get_clean(), 'info', true ); - echo '
'; - echo '

' . \esc_html__( 'Supported Types', 'functionalities' ) . '

'; - echo '

'; + ob_start(); + echo '

'; echo '' . \esc_html__( 'Site:', 'functionalities' ) . ' WebPage, AboutPage, ContactPage, Blog, SearchResultsPage
'; echo '' . \esc_html__( 'Article:', 'functionalities' ) . ' Article, BlogPosting, NewsArticle'; echo '

'; - echo '
'; + Admin_UI::render_docs_section( \__( 'Supported Types', 'functionalities' ), (string) ob_get_clean(), 'usage', true ); - echo '
'; - echo '

' . \esc_html__( 'For Developers', 'functionalities' ) . '

'; - echo '

'; + ob_start(); + echo '

'; echo \esc_html__( 'Filter:', 'functionalities' ) . ' functionalities_schema_enabled — ' . \esc_html__( 'toggle all schema output', 'functionalities' ) . '
'; echo \esc_html__( 'Filter:', 'functionalities' ) . ' functionalities_schema_site_type — ' . \esc_html__( 'modify site itemtype', 'functionalities' ) . '
'; echo \esc_html__( 'Filter:', 'functionalities' ) . ' functionalities_schema_article_type — ' . \esc_html__( 'modify article itemtype', 'functionalities' ) . '
'; echo \esc_html__( 'Filter:', 'functionalities' ) . ' functionalities_schema_content — ' . \esc_html__( 'modify wrapped content', 'functionalities' ); echo '

'; - echo '
'; + Admin_UI::render_docs_section( \__( 'For Developers', 'functionalities' ), (string) ob_get_clean(), 'developer', false ); }, 'functionalities_schema' ); @@ -704,30 +696,27 @@ function () { function () { echo '

' . \esc_html__( 'Create reusable CSS components that are automatically loaded across your entire site.', 'functionalities' ) . '

'; - echo '
'; - echo '

' . \esc_html__( 'What This Module Does', 'functionalities' ) . '

'; - echo '
    '; + ob_start(); + echo '
      '; echo '
    • ' . \esc_html__( 'Define CSS class names and their style rules in one place', 'functionalities' ) . '
    • '; echo '
    • ' . \esc_html__( 'Components are compiled into a single CSS file for optimal caching', 'functionalities' ) . '
    • '; echo '
    • ' . \esc_html__( 'Available on both frontend and admin pages', 'functionalities' ) . '
    • '; echo '
    • ' . \esc_html__( 'Includes default utility components like visually-hidden, skip-link, and marquee', 'functionalities' ) . '
    • '; echo '
    '; - echo '
'; + Admin_UI::render_docs_section( \__( 'What This Module Does', 'functionalities' ), (string) ob_get_clean(), 'info', true ); - echo '
'; - echo '

' . \esc_html__( 'How to Use', 'functionalities' ) . '

'; - echo '

' . \esc_html__( 'Add components by entering a CSS selector (e.g., .my-button) and CSS rules (e.g., background: blue; color: white;). Use the grid below to manage your components.', 'functionalities' ) . '

'; - echo '
'; + ob_start(); + echo '

' . \esc_html__( 'Add components by entering a CSS selector (e.g., .my-button) and CSS rules (e.g., background: blue; color: white;). Use the grid below to manage your components.', 'functionalities' ) . '

'; + Admin_UI::render_docs_section( \__( 'How to Use', 'functionalities' ), (string) ob_get_clean(), 'usage', true ); - echo '
'; - echo '

' . \esc_html__( 'For Developers', 'functionalities' ) . '

'; - echo '

'; + ob_start(); + echo '

'; echo \esc_html__( 'Filter:', 'functionalities' ) . ' functionalities_components_enabled — ' . \esc_html__( 'toggle output', 'functionalities' ) . '
'; echo \esc_html__( 'Filter:', 'functionalities' ) . ' functionalities_components_items — ' . \esc_html__( 'add components dynamically', 'functionalities' ) . '
'; echo \esc_html__( 'Filter:', 'functionalities' ) . ' functionalities_components_css — ' . \esc_html__( 'modify generated CSS', 'functionalities' ) . '
'; echo \esc_html__( 'Action:', 'functionalities' ) . ' functionalities_components_updated — ' . \esc_html__( 'fires when CSS file regenerates', 'functionalities' ); echo '

'; - echo '
'; + Admin_UI::render_docs_section( \__( 'For Developers', 'functionalities' ), (string) ob_get_clean(), 'developer', false ); }, 'functionalities_components' ); @@ -790,29 +779,26 @@ function () { function () { echo '

' . \esc_html__( 'Remove unnecessary WordPress features to improve performance and security.', 'functionalities' ) . '

'; - echo '
'; - echo '

' . \esc_html__( 'What This Module Does', 'functionalities' ) . '

'; - echo '
    '; + ob_start(); + echo '
      '; echo '
    • ' . \esc_html__( 'Remove bloat like emojis, oEmbeds, and unnecessary meta tags', 'functionalities' ) . '
    • '; echo '
    • ' . \esc_html__( 'Disable security concerns like XML-RPC and version disclosure', 'functionalities' ) . '
    • '; echo '
    • ' . \esc_html__( 'Improve performance by removing unused scripts and styles', 'functionalities' ) . '
    • '; echo '
    • ' . \esc_html__( 'Add useful enhancements like PrismJS and fullscreen textareas', 'functionalities' ) . '
    • '; echo '
    '; - echo '
'; + Admin_UI::render_docs_section( \__( 'What This Module Does', 'functionalities' ), (string) ob_get_clean(), 'info', true ); - echo '
'; - echo '

' . \esc_html__( 'Caution', 'functionalities' ) . '

'; - echo '

' . \esc_html__( 'Some options may break functionality if plugins depend on them. Test after enabling. Disable Heartbeat API with care if you use auto-save or real-time features.', 'functionalities' ) . '

'; - echo '
'; + ob_start(); + echo '

' . \esc_html__( 'Some options may break functionality if plugins depend on them. Test after enabling. Disable Heartbeat API with care if you use auto-save or real-time features.', 'functionalities' ) . '

'; + Admin_UI::render_docs_section( \__( 'Caution', 'functionalities' ), (string) ob_get_clean(), 'caution', true ); - echo '
'; - echo '

' . \esc_html__( 'For Developers', 'functionalities' ) . '

'; - echo '

'; + ob_start(); + echo '

'; echo \esc_html__( 'Filter:', 'functionalities' ) . ' functionalities_misc_options — ' . \esc_html__( 'modify options before application', 'functionalities' ) . '
'; echo \esc_html__( 'Filter:', 'functionalities' ) . ' functionalities_misc_disable_emojis — ' . \esc_html__( 'control emoji removal', 'functionalities' ) . '
'; echo \esc_html__( 'Filter:', 'functionalities' ) . ' functionalities_misc_disable_embeds — ' . \esc_html__( 'control embed removal', 'functionalities' ); echo '

'; - echo '
'; + Admin_UI::render_docs_section( \__( 'For Developers', 'functionalities' ), (string) ob_get_clean(), 'developer', false ); }, 'functionalities_misc' ); @@ -852,7 +838,7 @@ function () { function () { $opts = self::get_misc_options(); $val = isset( $opts['revisions_limit'] ) ? (int) $opts['revisions_limit'] : 10; - echo ' '; + echo ' '; echo \esc_html__( 'revisions per post. Applies when the option above is enabled. Zero keeps none.', 'functionalities' ); }, 'functionalities_misc', @@ -884,30 +870,27 @@ function () { function () { echo '

' . \esc_html__( 'Self-host custom fonts with automatic @font-face CSS generation.', 'functionalities' ) . '

'; - echo '
'; - echo '

' . \esc_html__( 'What This Module Does', 'functionalities' ) . '

'; - echo '
    '; + ob_start(); + echo '
      '; echo '
    • ' . \esc_html__( 'Generate @font-face CSS rules for self-hosted fonts', 'functionalities' ) . '
    • '; echo '
    • ' . \esc_html__( 'Support for variable fonts with weight ranges (e.g., 100 900)', 'functionalities' ) . '
    • '; echo '
    • ' . \esc_html__( 'WOFF2 format for modern browsers, optional WOFF fallback', 'functionalities' ) . '
    • '; echo '
    • ' . \esc_html__( 'Configurable font-display strategy (swap, auto, block, etc.)', 'functionalities' ) . '
    • '; echo '
    '; - echo '
'; + Admin_UI::render_docs_section( \__( 'What This Module Does', 'functionalities' ), (string) ob_get_clean(), 'info', true ); - echo '
'; - echo '

' . \esc_html__( 'How to Use', 'functionalities' ) . '

'; - echo '

' . \esc_html__( 'Upload font files to your media library or server, then add font entries below with the family name and file URLs. Use the generated font-family name in your CSS.', 'functionalities' ) . '

'; - echo '
'; + ob_start(); + echo '

' . \esc_html__( 'Upload font files to your media library or server, then add font entries below with the family name and file URLs. Use the generated font-family name in your CSS.', 'functionalities' ) . '

'; + Admin_UI::render_docs_section( \__( 'How to Use', 'functionalities' ), (string) ob_get_clean(), 'usage', true ); - echo '
'; - echo '

' . \esc_html__( 'For Developers', 'functionalities' ) . '

'; - echo '

'; + ob_start(); + echo '

'; echo \esc_html__( 'Filter:', 'functionalities' ) . ' functionalities_fonts_enabled — ' . \esc_html__( 'toggle output', 'functionalities' ) . '
'; echo \esc_html__( 'Filter:', 'functionalities' ) . ' functionalities_fonts_items — ' . \esc_html__( 'add fonts dynamically', 'functionalities' ) . '
'; echo \esc_html__( 'Filter:', 'functionalities' ) . ' functionalities_fonts_css — ' . \esc_html__( 'modify generated CSS', 'functionalities' ) . '
'; echo \esc_html__( 'Action:', 'functionalities' ) . ' functionalities_fonts_before_output'; echo '

'; - echo '
'; + Admin_UI::render_docs_section( \__( 'For Developers', 'functionalities' ), (string) ob_get_clean(), 'developer', false ); }, 'functionalities_fonts' ); @@ -963,15 +946,14 @@ function () { \__( 'Login Security Settings', 'functionalities' ), function () { echo '

' . \esc_html__( 'Protect your login page and customize its appearance.', 'functionalities' ) . '

'; - echo '
'; - echo '

' . \esc_html__( 'Security Features', 'functionalities' ) . '

'; - echo '
    '; + ob_start(); + echo '
      '; echo '
    • ' . \esc_html__( 'Limit failed login attempts to prevent brute force attacks', 'functionalities' ) . '
    • '; echo '
    • ' . \esc_html__( 'Disable XML-RPC authentication to block remote login attacks', 'functionalities' ) . '
    • '; echo '
    • ' . \esc_html__( 'Hide specific login errors to prevent username enumeration', 'functionalities' ) . '
    • '; echo '
    • ' . \esc_html__( 'Customize the login page with your logo and colors', 'functionalities' ) . '
    • '; echo '
    '; - echo '
'; + Admin_UI::render_docs_section( \__( 'Security Features', 'functionalities' ), (string) ob_get_clean(), 'info', true ); if ( \Functionalities\Features\Login_Security::lockouts_share_one_ip() ) { echo '
'; echo '' . \esc_html__( 'Every recent lockout came from the same address.', 'functionalities' ) . ' '; @@ -1264,7 +1246,7 @@ function () { 'cc-by-nc-nd' => 'CC BY-NC-ND 4.0', 'cc0' => 'CC0 1.0 (Public Domain)', ); - echo ''; foreach ( $licenses as $key => $label ) { $sel = selected( $val, $key, false ); echo ''; @@ -1282,7 +1264,7 @@ function () { function () { $o = self::get_meta_options(); $val = isset( $o['default_license_url'] ) ? (string) $o['default_license_url'] : ''; - echo ''; + echo ''; echo '

' . \esc_html__( 'Custom URL for "All Rights Reserved" license (e.g., your terms/disclaimer page).', 'functionalities' ) . '

'; }, 'functionalities_meta', @@ -1320,7 +1302,7 @@ function () { 'site' => \__( 'Site Name', 'functionalities' ), 'custom' => \__( 'Custom Name', 'functionalities' ), ); - echo ''; foreach ( $options as $key => $label ) { $sel = selected( $val, $key, false ); echo ''; @@ -1338,7 +1320,7 @@ function () { function () { $o = self::get_meta_options(); $val = isset( $o['custom_copyright_holder'] ) ? (string) $o['custom_copyright_holder'] : ''; - echo ''; + echo ''; echo '

' . \esc_html__( 'Used when "Custom Name" is selected above.', 'functionalities' ) . '

'; }, 'functionalities_meta', @@ -1352,7 +1334,7 @@ function () { $o = self::get_meta_options(); $val = isset( $o['dc_language'] ) ? (string) $o['dc_language'] : ''; $site_lang = \get_bloginfo( 'language' ); - echo ''; + echo ''; echo '

' . \esc_html__( 'Leave empty to use site language. Use ISO 639 codes (en, en-US, de, etc.).', 'functionalities' ) . '

'; }, 'functionalities_meta', @@ -1461,7 +1443,7 @@ function () { function () { $o = self::get_content_regression_options(); $val = isset( $o['link_drop_percent'] ) ? (int) $o['link_drop_percent'] : 30; - echo ' %'; + echo ' %'; echo '

' . \esc_html__( 'Warn if internal links drop by this percentage or more.', 'functionalities' ) . '

'; }, 'functionalities_content_regression', @@ -1474,7 +1456,7 @@ function () { function () { $o = self::get_content_regression_options(); $val = isset( $o['link_drop_absolute'] ) ? (int) $o['link_drop_absolute'] : 3; - echo ' ' . \esc_html__( 'links', 'functionalities' ); + echo ' ' . \esc_html__( 'links', 'functionalities' ); echo '

' . \esc_html__( 'Also warn if this many links are removed (whichever triggers first).', 'functionalities' ) . '

'; }, 'functionalities_content_regression', @@ -1524,7 +1506,7 @@ function () { function () { $o = self::get_content_regression_options(); $val = isset( $o['word_count_drop_percent'] ) ? (int) $o['word_count_drop_percent'] : 35; - echo ' %'; + echo ' %'; echo '

' . \esc_html__( 'Warn if word count drops by this percentage.', 'functionalities' ) . '

'; }, 'functionalities_content_regression', @@ -1537,7 +1519,7 @@ function () { function () { $o = self::get_content_regression_options(); $val = isset( $o['word_count_min_age_days'] ) ? (int) $o['word_count_min_age_days'] : 30; - echo ' ' . \esc_html__( 'days', 'functionalities' ); + echo ' ' . \esc_html__( 'days', 'functionalities' ); echo '

' . \esc_html__( 'Only check word count for posts older than this (to avoid alerts on new posts).', 'functionalities' ) . '

'; }, 'functionalities_content_regression', @@ -1637,7 +1619,7 @@ function () { function () { $o = self::get_content_regression_options(); $val = isset( $o['snapshot_rolling_count'] ) ? (int) $o['snapshot_rolling_count'] : 5; - echo ''; + echo ''; echo '

' . \esc_html__( 'Number of historical snapshots to retain per post.', 'functionalities' ) . '

'; }, 'functionalities_content_regression', diff --git a/includes/admin/trait-admin-svg-icons-ui.php b/includes/admin/trait-admin-svg-icons-ui.php index e1261a7..73c9dd2 100644 --- a/includes/admin/trait-admin-svg-icons-ui.php +++ b/includes/admin/trait-admin-svg-icons-ui.php @@ -33,22 +33,15 @@ public static function render_module_svg_icons( array $module ): void { } ?>
-

- - -

- - + + - + - -
- '; - foreach ( $docs['features'] as $feature ) { - $list .= '
  • ' . \esc_html( $feature ) . '
  • '; - } - $list .= ''; - Admin_UI::render_docs_section( \__( 'What This Module Does', 'functionalities' ), $list, 'info' ); - } - if ( ! empty( $docs['usage'] ) ) { - Admin_UI::render_docs_section( \__( 'How to Use', 'functionalities' ), '

    ' . \esc_html( $docs['usage'] ) . '

    ', 'usage' ); - } - if ( ! empty( $docs['hooks'] ) ) { - $hooks_html = '
    '; - foreach ( $docs['hooks'] as $hook ) { - $hooks_html .= '
    ' . \esc_html( $hook['name'] ) . '
    '; - $hooks_html .= '
    ' . \esc_html( $hook['description'] ) . '
    '; - } - $hooks_html .= '
    '; - Admin_UI::render_docs_section( \__( 'Developer Hooks', 'functionalities' ), $hooks_html, 'developer' ); - } - ?> -
    +
    @@ -116,7 +84,7 @@ public static function render_module_svg_icons( array $module ): void {

    ()

    - +

    @@ -146,6 +114,10 @@ public static function render_module_svg_icons( array $module ): void {
    +