commit 2f91c3e6af3456017c38270c4f6eebeae4b9161d
Author: marcusquinn <6428977+marcusquinn@users.noreply.github.com>
Date: Sat Oct 3 03:30:42 2026 +0100
WP Plugin Starter 1.0.0: a fresh start
The parts every wpallstars plugin needs: settings screen with search and an empty General tab, Read Me tab, features as classes, replaced plugins, the shared GitHub updater, release and check scripts, CI and the shared rules. scripts/rename-plugin.sh starts a new plugin; scripts/sync-core.sh keeps plugins' core files in step with this starter. Built with aidevops (https://aidevops.sh).
diff --git a/.aidevops.json b/.aidevops.json
new file mode 100644
index 0000000..672c8ab
--- /dev/null
+++ b/.aidevops.json
@@ -0,0 +1,14 @@
+{
+ "root_files": {
+ "allow": [
+ "readme.txt",
+ "changelog.txt",
+ "STANDARDS.md",
+ "DEVELOPMENT.md",
+ "RELEASING.md",
+ "LAUNCH.md",
+ "TESTING.md",
+ "STABILITY.md"
+ ]
+ }
+}
diff --git a/.distignore b/.distignore
new file mode 100644
index 0000000..20d85d3
--- /dev/null
+++ b/.distignore
@@ -0,0 +1,47 @@
+# Files excluded from the release builds (scripts/build-release.sh, wp dist-archive).
+# The WordPress.org build also leaves out the files in .distignore-wporg.
+.git
+.gitattributes
+.gitignore
+.distignore
+.distignore-wporg
+# WordPress.org SVN assets (banners) and their source; admin/images/banner.svg ships instead.
+.wordpress-org
+.woodpecker.yml
+# Development tools and CI (composer.json lists development tools only).
+.github
+.editorconfig
+.gitleaks.toml
+.aidevops.json
+composer.json
+composer.lock
+phpcs.xml.dist
+phpcs.xml
+phpstan.neon.dist
+phpstan.neon
+phpstan-baseline.neon
+phpstan-plugin.neon
+/dist
+.DS_Store
+.aider*
+# Local AI tooling state (aidevops), never part of the plugin.
+.agents
+.security-analysis
+*.bak
+.metadata_never_index
+.syncing
+AGENTS.md
+CONTRIBUTING.md
+DEVELOPMENT.md
+LAUNCH.md
+SECURITY.md
+STANDARDS.md
+RELEASING.md
+ROADMAP.md
+STABILITY.md
+TESTING.md
+/scripts
+reference-plugins
+project-documents
+node_modules
+/vendor
diff --git a/.distignore-wporg b/.distignore-wporg
new file mode 100644
index 0000000..884bc98
--- /dev/null
+++ b/.distignore-wporg
@@ -0,0 +1,8 @@
+# Files left out of the WordPress.org build, on top of .distignore
+# (rsync exclude patterns; a leading / anchors to the plugin folder).
+# Plugins hosted on WordPress.org may not install or update code from
+# anywhere else, so updates from GitHub stay in GitHub releases only.
+# scripts/build-release.sh also removes the Git Updater header lines
+# (GitHub Plugin URI, Primary Branch, Release Asset) from wp-plugin-starter-template.php.
+/includes/github-updater/
+# Keep README.md: the admin Read Me tab shows it (admin/data/readme.php).
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..dababf6
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,15 @@
+# Opt out of AI model training
+* ai-training=false
+
+# Keep development files out of release archives (git archive).
+/.distignore export-ignore
+/.distignore-wporg export-ignore
+/.gitattributes export-ignore
+/.gitignore export-ignore
+/.woodpecker.yml export-ignore
+/DEVELOPMENT.md export-ignore
+/RELEASING.md export-ignore
+/ROADMAP.md export-ignore
+/STABILITY.md export-ignore
+/TESTING.md export-ignore
+/scripts export-ignore
diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml
new file mode 100644
index 0000000..5f7db55
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.yml
@@ -0,0 +1,62 @@
+name: Bug report
+description: Something in WP Plugin Starter does not work as it should.
+labels: ["bug"]
+body:
+ - type: markdown
+ attributes:
+ value: |
+ Security problems: please do not report them here. See SECURITY.md.
+ - type: textarea
+ id: what
+ attributes:
+ label: What happened
+ description: What you saw, and what you expected instead.
+ validations:
+ required: true
+ - type: textarea
+ id: steps
+ attributes:
+ label: Steps to reproduce
+ description: The feature involved, its settings, and what you clicked.
+ placeholder: |
+ 1. Turn on …
+ 2. Open …
+ 3. See …
+ validations:
+ required: true
+ - type: input
+ id: version
+ attributes:
+ label: WP Plugin Starter version
+ description: Shown on the Plugins screen.
+ validations:
+ required: true
+ - type: dropdown
+ id: source
+ attributes:
+ label: Installed from
+ options:
+ - GitHub release
+ - WordPress.org
+ - Other
+ validations:
+ required: true
+ - type: input
+ id: environment
+ attributes:
+ label: WordPress and PHP versions
+ description: Tools → Site Health → Info shows both.
+ placeholder: WordPress 6.8, PHP 8.3
+ validations:
+ required: true
+ - type: textarea
+ id: log
+ attributes:
+ label: Messages in debug.log
+ description: Lines from wp-content/debug.log that mention wpstarter, if any. Remove anything private first.
+ render: text
+ - type: textarea
+ id: more
+ attributes:
+ label: Anything else
+ description: Theme, other plugins involved, multisite, screenshots.
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
new file mode 100644
index 0000000..1a528a5
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -0,0 +1,5 @@
+blank_issues_enabled: false
+contact_links:
+ - name: Security problem
+ url: https://github.com/wpallstars/wp-plugin-starter-template-for-ai-coding/blob/main/SECURITY.md
+ about: Report security problems privately, never in a public issue.
diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml
new file mode 100644
index 0000000..c83234b
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/feature_request.yml
@@ -0,0 +1,27 @@
+name: Feature request
+description: Suggest a feature or an improvement.
+labels: ["enhancement"]
+body:
+ - type: textarea
+ id: problem
+ attributes:
+ label: The problem
+ description: What is slow, missing or annoying today, and for whom.
+ validations:
+ required: true
+ - type: textarea
+ id: idea
+ attributes:
+ label: What WP Plugin Starter could do
+ description: What it would do on its own, without new settings where possible.
+ validations:
+ required: true
+ - type: input
+ id: replaces
+ attributes:
+ label: Plugin it would replace
+ description: If another plugin does this now, its name and WordPress.org page.
+ - type: textarea
+ id: more
+ attributes:
+ label: Anything else
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 0000000..b6ca0a1
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,21 @@
+# Weekly updates for the pinned GitHub Actions and the Composer development
+# tools (composer.json; the plugin itself has no Composer dependencies).
+# Each ecosystem's updates come grouped in one pull request.
+version: 2
+updates:
+ - package-ecosystem: github-actions
+ directory: /
+ schedule:
+ interval: weekly
+ groups:
+ actions:
+ patterns: ["*"]
+
+ - package-ecosystem: composer
+ directory: /
+ schedule:
+ interval: weekly
+ versioning-strategy: lockfile-only
+ groups:
+ dev-tools:
+ patterns: ["*"]
diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md
new file mode 100644
index 0000000..1644aca
--- /dev/null
+++ b/.github/pull_request_template.md
@@ -0,0 +1,19 @@
+## What and why
+
+
+
+## How it was tested
+
+
+
+- [ ] `scripts/lint.sh` passes.
+- [ ] Tested the changed feature on a WordPress site, with no new messages
+ mentioning `wpstarter` in `debug.log`.
+- [ ] WordPress 6.2 with PHP 7.4, if this uses a core function.
+- [ ] Kadence light and dark mode, if this changes front-end styles.
+
+## Checklist
+
+- [ ] Changelog lines in `README.md`, `changelog.txt` and `readme.txt`.
+- [ ] New options, post meta, transients or cron hooks are removed in `uninstall.php`.
+- [ ] New features are off by default.
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..c685006
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,138 @@
+# Checks for every pull request and every push to main. Run the same checks
+# locally with scripts/lint.sh, scripts/preflight-release.sh,
+# scripts/plugin-check.sh and scripts/smoke-test.sh (DEVELOPMENT.md).
+#
+# Lint runs on every push. The release and smoke-test jobs take longer, so
+# draft pull requests skip them until they are marked ready for review.
+# Actions are pinned to commit SHAs; Dependabot keeps them up to date.
+name: CI
+
+on:
+ pull_request:
+ types: [opened, synchronize, reopened, ready_for_review]
+ push:
+ branches: [main]
+ workflow_dispatch:
+
+permissions:
+ contents: read
+
+concurrency:
+ group: ci-${{ github.event.pull_request.number || github.ref }}
+ cancel-in-progress: true
+
+jobs:
+ lint:
+ name: Lint (PHP 7.4)
+ runs-on: ubuntu-latest
+ timeout-minutes: 15
+ steps:
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+ with:
+ persist-credentials: false
+
+ - uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2
+ with:
+ php-version: "7.4"
+ coverage: none
+ tools: composer:v2
+
+ - name: Composer cache folder
+ id: composer-cache
+ run: echo "dir=$(composer config cache-files-dir)" >> "$GITHUB_OUTPUT"
+
+ - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
+ with:
+ path: ${{ steps.composer-cache.outputs.dir }}
+ key: composer-${{ hashFiles('composer.lock') }}
+ restore-keys: composer-
+
+ - name: Install development tools
+ run: composer install --no-interaction --no-progress --prefer-dist
+
+ # PHPStan's result cache: the latest run's is restored, so only files
+ # changed since then are analysed again.
+ - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
+ with:
+ path: .cache/phpstan
+ key: phpstan-${{ github.run_id }}
+ restore-keys: phpstan-
+
+ - name: PHP syntax (7.4), JavaScript syntax, ShellCheck, PHPCS, PHPStan
+ run: scripts/lint.sh php js shell phpcs phpstan
+
+ - name: actionlint
+ run: docker run --rm -v "$PWD:/repo" --workdir /repo rhysd/actionlint:1.7.12 -color
+
+ release:
+ name: Release build, preflight and Plugin Check
+ needs: lint
+ if: github.event_name != 'pull_request' || !github.event.pull_request.draft
+ runs-on: ubuntu-latest
+ timeout-minutes: 20
+ steps:
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+ with:
+ fetch-depth: 0
+ persist-credentials: false
+
+ - uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2
+ with:
+ php-version: "7.4"
+ coverage: none
+ tools: none
+
+ - name: Preflight
+ run: scripts/preflight-release.sh --offline --no-docker
+
+ - name: Build both zips
+ run: scripts/build-release.sh --out dist
+
+ - name: Plugin Check
+ run: |
+ args=()
+ for zip in dist/*.zip; do
+ args+=(--zip "$zip")
+ done
+ scripts/plugin-check.sh "${args[@]}" --keep-output dist/plugin-check
+
+ - name: Keep the zips and reports for testing
+ if: always()
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
+ with:
+ name: ${{ github.event.repository.name }}-build-${{ github.sha }}
+ path: dist/
+ retention-days: 7
+ if-no-files-found: ignore
+
+ smoke:
+ name: Smoke test (WordPress ${{ matrix.wp }}, PHP ${{ matrix.php }})
+ needs: lint
+ if: github.event_name != 'pull_request' || !github.event.pull_request.draft
+ runs-on: ubuntu-latest
+ timeout-minutes: 20
+ strategy:
+ fail-fast: false
+ matrix:
+ include:
+ # Minimums (readme.txt, plugin header).
+ - wp: "6.2"
+ php: "7.4"
+ - wp: latest
+ php: "8.3"
+ steps:
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+ with:
+ persist-credentials: false
+
+ - name: Smoke test
+ run: scripts/smoke-test.sh --wp "${{ matrix.wp }}" --php "${{ matrix.php }}" --keep-log debug.log
+
+ - name: Keep debug.log
+ if: failure()
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
+ with:
+ name: debug-log-wp${{ matrix.wp }}-php${{ matrix.php }}
+ path: debug.log
+ retention-days: 7
+ if-no-files-found: ignore
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..c1d9568
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,73 @@
+# macOS
+.DS_Store
+.AppleDouble
+.LSOverride
+Icon
+._*
+.DocumentRevisions-V100
+.fseventsd
+.Spotlight-V100
+.TemporaryItems
+.Trashes
+.VolumeIcon.icns
+.com.apple.timemachine.donotpresent
+.AppleDB
+.AppleDesktop
+Network Trash Folder
+Temporary Items
+.apdisk
+
+# Windows
+Thumbs.db
+Thumbs.db:encryptable
+ehthumbs.db
+ehthumbs_vista.db
+*.stackdump
+[Dd]esktop.ini
+$RECYCLE.BIN/
+*.cab
+*.msi
+*.msix
+*.msm
+*.msp
+*.lnk
+
+# Development & Local Environment
+.aider*
+.git_status_output.txt
+.git_status_temp.txt
+project-documents/
+reference-plugins/
+*.bak*
+node_modules/
+package-lock.json
+.cache/
+vendor/
+
+# WordPress specific
+*.log
+wp-config.php
+wp-content/advanced-cache.php
+wp-content/backup-db/
+wp-content/backups/
+wp-content/blogs.dir/
+wp-content/cache/
+wp-content/upgrade/
+wp-content/uploads/
+
+# IDE & Editors
+.vscode/
+.idea/
+*.sublime-project
+*.sublime-workspace
+*.swp
+*.swo
+
+# Keep .woodpecker.yml if needed for CI/CD
+# .woodpecker.yml
+
+# aidevops runtime state
+.agents/loop-state/
+
+# Release builds (scripts/build-release.sh)
+/dist/
\ No newline at end of file
diff --git a/.gitleaks.toml b/.gitleaks.toml
new file mode 100644
index 0000000..789ac0c
--- /dev/null
+++ b/.gitleaks.toml
@@ -0,0 +1,13 @@
+# gitleaks: secret scan of the Git history (DEVELOPMENT.md → Secrets in history).
+# Keep gitleaks' own rules; list only checked false positives here, each with
+# its reason and as narrow as possible.
+
+[extend]
+useDefault = true
+
+[[allowlists]]
+description = "Fluent Forms field keys (el_ and a uniqid()) in starter forms, not secrets"
+condition = "AND"
+paths = ['''^starters/fluentform\.json$''']
+regexTarget = "line"
+regexes = ['''"uniqElKey": "el_[0-9.]+"''']
diff --git a/.wordpress-org/banner-1544x500.png b/.wordpress-org/banner-1544x500.png
new file mode 100644
index 0000000..6648a28
Binary files /dev/null and b/.wordpress-org/banner-1544x500.png differ
diff --git a/.wordpress-org/banner-772x250.png b/.wordpress-org/banner-772x250.png
new file mode 100644
index 0000000..3f1540b
Binary files /dev/null and b/.wordpress-org/banner-772x250.png differ
diff --git a/.wordpress-org/banner.svg b/.wordpress-org/banner.svg
new file mode 100644
index 0000000..1af82de
--- /dev/null
+++ b/.wordpress-org/banner.svg
@@ -0,0 +1,129 @@
+
+
+
diff --git a/AGENTS.md b/AGENTS.md
new file mode 100644
index 0000000..954630d
--- /dev/null
+++ b/AGENTS.md
@@ -0,0 +1,51 @@
+# WP Plugin Starter — agent guide
+
+The wpallstars starter plugin: what every wpallstars plugin is made from.
+It has no features of its own, only the parts every plugin needs.
+
+**Read `STANDARDS.md` before any change.** It holds the rules every plugin
+made from the starter shares: structure and core files, code rules, Updates
+from GitHub, releases, front-end styling and dark mode, and testing. This
+repository holds the master copy of it and of every core file
+(`scripts/core-files.txt`). This file holds only what is the starter's own.
+
+| Placeholder in `STANDARDS.md` | WP Plugin Starter |
+|---|---|
+| `{slug}` | `wp-plugin-starter-template` (main file `wp-plugin-starter-template.php`) |
+| `{prefix}` | `wpstarter` |
+| `{Prefix}` | `WPStarter` |
+| `{PREFIX}` | `WPSTARTER` |
+| `{Name}` | WP Plugin Starter |
+| `{css}` | `wps` |
+
+User docs: `README.md` (developers, and the Read Me tab) and `readme.txt`.
+Development: `DEVELOPMENT.md`. Releases: `RELEASING.md`; launch state:
+`LAUNCH.md`.
+
+## What belongs here
+
+- Only what every plugin needs. A part one plugin needs stays in that
+ plugin (its `{Prefix}_Setup`, a feature or its own files). When a lesson
+ from a plugin applies to all of them, change the core file here first,
+ then run `scripts/sync-core.sh` in each plugin (`--check` lists what
+ differs). Keep core files free of any one plugin's names, examples and
+ paths: this repository is public.
+- `includes/class-wpstarter-setup.php` stays empty of features, so the
+ settings screen shows the empty General tab and the Read Me tab. Test a
+ core change in a plugin that uses it, then here.
+- `scripts/rename-plugin.sh` must keep working on a fresh copy: after a
+ change to names in core files, try it on a throwaway clone (rename, lint,
+ smoke test).
+- The banner source is `.wordpress-org/banner.svg`: the wpallstars plugin
+ stack picture in red only, on wpallstars branding. Rebuild it with
+ `scripts/build-banner.sh`.
+- This repository is public. Never name private repositories, their
+ issues, or local paths in it (commits, docs, comments or examples).
+- Every plugin made from the starter keeps the **Built with AI** credit to
+ aidevops (https://aidevops.sh) in `README.md` and `readme.txt`.
+
+## Test sites
+
+Install the GitHub build on a local test site with
+`scripts/preview-site.sh` (see `DEVELOPMENT.md`), or install the GitHub
+zip from `scripts/build-release.sh` on any test site.
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..c9ea0ee
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,36 @@
+# Contributing to WP Plugin Starter
+
+Thank you for helping. Bug reports, fixes and ideas are all welcome.
+
+## Reporting a problem
+
+Open an issue with the **Bug report** form. Include the versions, the steps
+and any messages from `wp-content/debug.log`. Security problems go through
+`SECURITY.md`, never a public issue.
+
+## Suggesting a feature
+
+Open an issue with the **Feature request** form before writing code. Each
+setting makes a choice for the plugin's users, so a new one has to earn its
+place: say what the feature does on its own and what problem it solves.
+
+## Pull requests
+
+1. Read `STANDARDS.md` (how features, settings, migrations and front-end
+ styles work, the code rules and testing), `AGENTS.md` (what is this
+ plugin's own) and `DEVELOPMENT.md` (set-up and checks).
+2. Keep each pull request to one change. Features are off by default.
+3. Run `scripts/lint.sh` and fix what it finds in the code.
+4. Test on a real WordPress site, including WordPress 6.2 with PHP 7.4 if
+ you use a core function, and Kadence light and dark mode if you change
+ front-end styles.
+5. Add a line to the changelogs: `README.md` (details), `changelog.txt`
+ (for users) and `readme.txt` (one short line).
+
+CI runs the checks on every pull request. Write the pull request description
+so a reviewer knows what changed, why, and how you tested it.
+
+## Licence
+
+WP Plugin Starter is GPL-2.0-or-later. By contributing, you agree your work is
+released under the same licence.
diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md
new file mode 100644
index 0000000..9b0c250
--- /dev/null
+++ b/DEVELOPMENT.md
@@ -0,0 +1,206 @@
+# Development
+
+How changes are made and checked in every plugin made from the wpallstars
+starter plugin. This file is the same in each of them (names as
+placeholders: `STANDARDS.md` lists them, and the plugin's `AGENTS.md` gives
+its values). Rules for features, code, styling and testing: `STANDARDS.md`;
+the plugin's own rules: `AGENTS.md`. Releases: `RELEASING.md`.
+
+## Workflow
+
+1. Open or pick an issue that says what should change and how to check it.
+2. Work on a branch in its own worktree, never on `main`. Branch names:
+ `feature/…`, `bugfix/…`, `chore/…`.
+3. Commit small, working steps. Run `scripts/lint.sh` before pushing.
+4. Push and open a pull request (`Resolves #N`). Open it as a draft while
+ the work is in progress: CI lints every push, and the longer release and
+ smoke-test jobs start when the pull request is marked ready for review.
+5. Check the change on the shared preview site (`scripts/preview-site.sh`,
+ `STANDARDS.md` → Testing), in light and dark mode for front-end styles.
+6. Merge once CI passes. Releases are separate: `RELEASING.md`.
+
+## Set up
+
+Needs PHP 7.4 or later, Composer 2, Node.js (syntax checks only),
+ShellCheck and Docker (release checks and smoke test). actionlint is
+optional locally; CI runs it.
+
+```bash
+composer install
+```
+
+`composer.json` lists development tools only. The plugin has no Composer
+dependencies, and `vendor/`, `composer.*`, the tool configuration and
+`.github/` are left out of release zips (`.distignore`; the preflight fails
+if one gets in).
+
+## Checks
+
+Every pull request and every push to `main` runs these in GitHub Actions
+(`.github/workflows/ci.yml`). Each one runs the same way locally.
+
+| Check | Command | What it finds |
+| --- | --- | --- |
+| PHP syntax | `scripts/lint.sh php` | Syntax errors; CI uses PHP 7.4, the minimum. |
+| JavaScript syntax | `scripts/lint.sh js` | Syntax errors (`node --check`). |
+| Shell scripts | `scripts/lint.sh shell` | ShellCheck findings in `scripts/`. |
+| Workflows | `scripts/lint.sh workflows` | actionlint findings in `.github/workflows/`. |
+| Coding standards | `scripts/lint.sh phpcs` | WordPress Coding Standards: escaping, sanitising, nonces, prepared SQL, i18n, PHP 7.4 and WordPress 6.2 compatibility (`phpcs.xml.dist`). |
+| Static analysis | `scripts/lint.sh phpstan` | Unknown functions, classes and methods, wrong argument counts and types, dead code (PHPStan level 5, `phpstan.neon.dist`). |
+| Release build | `scripts/preflight-release.sh --offline` | Versions, headers, `readme.txt`, presets (where the plugin has them) and the contents of both zips. |
+| Plugin Check | `scripts/plugin-check.sh` | The WordPress.org review tool, on both zips. |
+| Smoke test | `scripts/smoke-test.sh --wp 6.2 --php 7.4` and `scripts/smoke-test.sh` | Installs the GitHub zip, loads the site and admin screens with default settings and with every feature on, runs cron, uninstalls. Fails on any PHP message, a failed page or leftover options. |
+
+`scripts/lint.sh` with no arguments runs the first six.
+
+The scripts work out which plugin they are in from its main file
+(`scripts/lib/plugin.sh`): the PHP file at the top of the repository with a
+`Plugin Name:` header gives the slug (its file name), the name, the class
+prefix (`@package`) and the constant prefix (`define('_VERSION', …)`).
+Environment overrides use that prefix, for example
+`{PREFIX}_PREVIEW_SITE` or `{PREFIX}_DB_IMAGE`. Checks for parts only some
+plugins have (presets and starter data, the replaced plugins count) run
+when their files exist, so the same scripts serve every plugin made from the
+starter.
+
+### Coding standards
+
+`phpcs.xml.dist` uses the WordPress ruleset. The plugin's own style
+differs from it in a few places (spacing, `array()`, file names, Yoda
+conditions), and those sniffs are off; the comments in the file say why.
+Security, database and compatibility sniffs stay on. Fix findings in the
+code. Where a finding is intended, add an inline
+`// phpcs:ignore Sniff.Name -- reason` on that line only.
+
+`vendor/bin/phpcbf` fixes what it can automatically.
+
+### Static analysis
+
+PHPStan reads the code with WordPress's stubs and PHP 7.4's functions.
+`scripts/phpstan-bootstrap.php` defines the constants WordPress and the
+plugin set while loading. Classes and functions of other plugins the code
+uses (WP-CLI, WooCommerce and the like) are ignored in `phpstan-plugin.neon`,
+because the code uses them only after checking they are loaded.
+
+`phpstan-baseline.neon` is empty: every finding fails the check. Fix the
+code. Where PHPStan or the stubs are wrong (a custom `wp_hash()` scheme, a
+check for a method newer WordPress versions have, variables a closure
+changes by reference), add an entry under `ignoreErrors` with the
+identifier, the file and the reason: in `phpstan.neon.dist` for the files
+every plugin made from the starter shares, in `phpstan-plugin.neon` for the
+plugin's own files and extra paths. Never put findings in the baseline to
+get a change through.
+
+### Secrets in history
+
+Scan the whole Git history before the repository goes public, and after
+importing code from elsewhere:
+
+```bash
+docker run --rm -v "$PWD:/repo:ro" ghcr.io/gitleaks/gitleaks:latest git /repo --redact --no-banner
+```
+
+In a linked worktree, also mount the main repository's `.git` folder at
+the same path. `.gitleaks.toml` lists the false positives with reasons. A
+real secret is rotated first, then removed from history.
+
+### Smoke test
+
+`scripts/smoke-test.sh` starts a throwaway WordPress in Docker (MariaDB,
+Apache and WP-CLI images for the chosen PHP version), so nothing touches
+the shared preview site. CI runs it on WordPress 6.2 with PHP 7.4 and on
+the latest WordPress with PHP 8.3. Every feature with an on/off setting is
+switched on at once, except one with the key `maintenance` (a maintenance
+mode would answer every visitor page with its notice). `--keep-log FILE` saves `debug.log`; CI keeps it as an artifact
+when the test fails.
+
+## Test site resources
+
+Test sites run the plugin alongside many others (a plugin that recommends
+plugins is tested with all of them active). PHP's defaults are too small for that: OPcache fills
+and restarts, admin requests queue behind two workers, and the slowdowns
+look like bugs. Size every test site, shared or throwaway, with room to
+spare:
+
+| Setting | Where (LocalWP, nginx) | Value | PHP or Local default |
+|---|---|---|---|
+| `opcache.memory_consumption` | `conf/php/php.ini.hbs` | `1024` | 128 |
+| `opcache.interned_strings_buffer` | `conf/php/php.ini.hbs` | `64` | 8 |
+| `opcache.max_accelerated_files` | `conf/php/php.ini.hbs` | `50000` | 10000 |
+| `memory_limit` | `conf/php/php.ini.hbs` | `768M` | 256M |
+| `pm.max_children` | `conf/php/php-fpm.d/www.conf.hbs` | `5` | 2 |
+
+Measured on a shared test site with 72 plugins installed: OPcache used
+509 MB of 512 MB and restarted, then 315 MB of 1 GB with 31,723 PHP files
+cached; pages peaked at 352 MB of memory; the busiest hour needed 5 workers.
+
+- LocalWP keeps these per site in `/conf/`. Edit the `.hbs`
+ templates, not the generated files under Local's `run/` folder, then
+ stop and start the site in Local: it rebuilds the configuration only on
+ start. Add a comment with the date and reason next to each change.
+- The OPcache lines sit inside `{{#unless apache}}`: on an Apache site,
+ move them out or set them in Apache's own configuration.
+- Check after the restart: Site Health → Info → Server shows the new values.
+- Raise them again when a site needs more, and update this table in the
+ starter.
+- To test low-resource warnings, lower a value on a throwaway site, never
+ the shared one.
+- Docker sites (`scripts/smoke-test.sh`, step 4 in `STANDARDS.md` →
+ Testing) take the same values in a `.ini` file mounted into
+ `/usr/local/etc/php/conf.d/`.
+
+## Dependencies
+
+Dependabot (`.github/dependabot.yml`) opens one pull request a week for
+the GitHub Actions and one for the Composer tools. Actions are pinned to
+commit SHAs with the version in a comment; keep it that way when editing
+workflows. Do not add third-party actions that only save a few lines of
+shell.
+
+## While private: checks are advisory
+
+The repository is private until it is released, so Actions minutes cost
+money and most code-review services are paid. Until then, work moves fast:
+
+- No branch protection or required checks on `main`. A failing check does
+ not block a merge, so a merge never waits on an unrelated failure.
+- Fix any failure your change causes before merging. For a failure your
+ change did not cause, open an issue with the run link and the first error,
+ and merge anyway.
+- Draft pull requests run only the lint job. A new push cancels the run for
+ the previous one.
+- The build zips and Plugin Check reports are kept for seven days on each
+ run (artifact `-build-…`) for testing a branch on a site.
+- Review apps that are already installed (CodeRabbit, qlty, Socket) give
+ advice only. A rate-limited or missing review never holds up a merge.
+
+## At public launch: full sweep
+
+Making the repository public needs the owner's say. Do this sweep in the
+same step, while the code-review services are free for
+public repositories. It goes through the whole codebase once, then keeps
+it at that standard:
+
+1. Turn on the free reviewers for the whole codebase, not just new
+ changes: CodeRabbit full review, Codacy, SonarCloud (SonarQube Cloud)
+ and qlty, plus GitHub's CodeQL (PHP and JavaScript), Dependabot
+ security alerts, secret scanning with push protection, and OpenSSF
+ Scorecard. Socket keeps checking dependencies.
+2. Fix what they find in the code, in small pull requests by area
+ (security first). Each finding is either fixed, explained in an inline
+ comment, or marked as a false positive in that service with the reason.
+3. Raise the PHPStan level one step at a time (6, then higher if the
+ findings are real bugs and not noise). The baseline is already empty.
+4. Require the CI checks on `main` (Lint, Release build, both Smoke
+ tests) with a branch ruleset, without "branch must be up to date": the
+ checks are fast, and changelog lines conflict on every merge.
+5. Turn on private vulnerability reporting (Settings → Security), which
+ `SECURITY.md` asks reporters to use, and add the CI badge to
+ `README.md`. `SECURITY.md`, `CONTRIBUTING.md` and the issue and pull
+ request templates are already in place.
+6. Run `workflows/public-launch-checklist.md` from the AI DevOps framework
+ for anything public: no private paths, site names or secrets in the code,
+ history, issues or docs. Run the history scan above again for commits
+ made since the last one.
+
+What a plugin has already done ahead of launch is in its `LAUNCH.md`.
diff --git a/LAUNCH.md b/LAUNCH.md
new file mode 100644
index 0000000..96b9e29
--- /dev/null
+++ b/LAUNCH.md
@@ -0,0 +1,9 @@
+# WP Plugin Starter — launch state
+
+The starter is released on GitHub only. It is not meant for WordPress.org:
+it has no features, and plugins made from it are submitted under their own
+names. `RELEASING.md` → WordPress.org applies to those plugins, each with its
+own `LAUNCH.md`.
+
+`scripts/preflight-release.sh` and `scripts/plugin-check.sh` still run on
+every release, so the core files stay ready for a plugin's submission.
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..9efa6fb
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,338 @@
+ GNU GENERAL PUBLIC LICENSE
+ Version 2, June 1991
+
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
+
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The licenses for most software are designed to take away your
+freedom to share and change it. By contrast, the GNU General Public
+License is intended to guarantee your freedom to share and change free
+software--to make sure the software is free for all its users. This
+General Public License applies to most of the Free Software
+Foundation's software and to any other program whose authors commit to
+using it. (Some other Free Software Foundation software is covered by
+the GNU Lesser General Public License instead.) You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+this service if you wish), that you receive source code or can get it
+if you want it, that you can change the software or use pieces of it
+in new free programs; and that you know you can do these things.
+
+ To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must give the recipients all the rights that
+you have. You must make sure that they, too, receive or can get the
+source code. And you must show them these terms so they know their
+rights.
+
+ We protect your rights with two steps: (1) copyright the software, and
+(2) offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+
+ Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software. If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+
+ Finally, any free program is threatened constantly by software
+patents. We wish to avoid the danger that redistributors of a free
+program will individually obtain patent licenses, in effect making the
+program proprietary. To prevent this, we have made it clear that any
+patent must be licensed for everyone's free use or not licensed at all.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ GNU GENERAL PUBLIC LICENSE
+ TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+ 0. This License applies to any program or other work which contains
+a notice placed by the copyright holder saying it may be distributed
+under the terms of this General Public License. The "Program", below,
+refers to any such program or work, and a "work based on the Program"
+means either the Program or any derivative work under copyright law:
+that is to say, a work containing the Program or a portion of it,
+either verbatim or with modifications and/or translated into another
+language. (Hereinafter, translation is included without limitation in
+the term "modification".) Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope. The act of
+running the Program is not restricted, and the output from the Program
+is covered only if its contents constitute a work based on the
+Program (independent of having been made by running the Program).
+Whether that is true depends on what the Program does.
+
+ 1. You may copy and distribute verbatim copies of the Program's
+source code as you receive it, in any medium, provided that you
+conspicuously and appropriately publish on each copy an appropriate
+copyright notice and disclaimer of warranty; keep intact all the
+notices that refer to this License and to the absence of any warranty;
+and give any other recipients of the Program a copy of this License
+along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and
+you may at your option offer warranty protection in exchange for a fee.
+
+ 2. You may modify your copy or copies of the Program or any portion
+of it, thus forming a work based on the Program, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+ a) You must cause the modified files to carry prominent notices
+ stating that you changed the files and the date of any change.
+
+ b) You must cause any work that you distribute or publish, that in
+ whole or in part contains or is derived from the Program or any
+ part thereof, to be licensed as a whole at no charge to all third
+ parties under the terms of this License.
+
+ c) If the modified program normally reads commands interactively
+ when run, you must cause it, when started running for such
+ interactive use in the most ordinary way, to print or display an
+ announcement including an appropriate copyright notice and a
+ notice that there is no warranty (or else, saying that you provide
+ a warranty) and that users may redistribute the program under
+ these conditions, and telling the user how to view a copy of this
+ License. (Exception: if the Program itself is interactive but
+ does not normally print such an announcement, your work based on
+ the Program is not required to print an announcement.)
+
+These requirements apply to the modified work as a whole. If
+identifiable sections of that work are not derived from the Program,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works. But when you
+distribute the same sections as part of a whole which is a work based
+on the Program, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Program.
+
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+ 3. You may copy and distribute the Program (or a work based on it,
+under Section 2) in object code or executable form under the terms of
+Sections 1 and 2 above provided that you also do one of the following:
+
+ a) Accompany it with the complete corresponding machine-readable
+ source code, which must be distributed under the terms of Sections
+ 1 and 2 above on a medium customarily used for software interchange; or,
+
+ b) Accompany it with a written offer, valid for at least three
+ years, to give any third party, for a charge no more than your
+ cost of physically performing source distribution, a complete
+ machine-readable copy of the corresponding source code, to be
+ distributed under the terms of Sections 1 and 2 above on a medium
+ customarily used for software interchange; or,
+
+ c) Accompany it with the information you received as to the offer
+ to distribute corresponding source code. (This alternative is
+ allowed only for noncommercial distribution and only if you
+ received the program in object code or executable form with such
+ an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for
+making modifications to it. For an executable work, complete source
+code means all the source code for all modules it contains, plus any
+associated interface definition files, plus the scripts used to
+control compilation and installation of the executable. However, as a
+special exception, the source code distributed need not include
+anything that is normally distributed (in either source or binary
+form) with the major components (compiler, kernel, and so on) of the
+operating system on which the executable runs, unless that component
+itself accompanies the executable.
+
+If distribution of executable or object code is made by offering
+access to copy from a designated place, then offering equivalent
+access to copy the source code from the same place counts as
+distribution of the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+ 4. You may not copy, modify, sublicense, or distribute the Program
+except as expressly provided under this License. Any attempt
+otherwise to copy, modify, sublicense or distribute the Program is
+void, and will automatically terminate your rights under this License.
+However, parties who have received copies, or rights, from you under
+this License will not have their licenses terminated so long as such
+parties remain in full compliance.
+
+ 5. You are not required to accept this License, since you have not
+signed it. However, nothing else grants you permission to modify or
+distribute the Program or its derivative works. These actions are
+prohibited by law if you do not accept this License. Therefore, by
+modifying or distributing the Program (or any work based on the
+Program), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+ 6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the
+original licensor to copy, distribute or modify the Program subject to
+these terms and conditions. You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+
+ 7. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Program at all. For example, if a patent
+license would not permit royalty-free redistribution of the Program by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply and the section as a whole is intended to apply in other
+circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system, which is
+implemented by public license practices. Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+ 8. If the distribution and/or use of the Program is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Program under this License
+may add an explicit geographical distribution limitation excluding
+those countries, so that distribution is permitted only in or among
+countries not thus excluded. In such case, this License incorporates
+the limitation as if written in the body of this License.
+
+ 9. The Free Software Foundation may publish revised and/or new versions
+of the General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+Each version is given a distinguishing version number. If the Program
+specifies a version number of this License which applies to it and "any
+later version", you have the option of following the terms and conditions
+either of that version or of any later version published by the Free
+Software Foundation. If the Program does not specify a version number of
+this License, you may choose any version ever published by the Free Software
+Foundation.
+
+ 10. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, write to the author
+to ask for permission. For software which is copyrighted by the Free
+Software Foundation, write to the Free Software Foundation; we sometimes
+make exceptions for this. Our decision will be guided by the two goals
+of preserving the free status of all derivatives of our free software and
+of promoting the sharing and reuse of software generally.
+
+ NO WARRANTY
+
+ 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
+FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
+OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
+TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
+REPAIR OR CORRECTION.
+
+ 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
+OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
+TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
+YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
+PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGES.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+
+ Copyright (C)
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 2 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along
+ with this program; if not, see .
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this
+when it starts in an interactive mode:
+
+ Gnomovision version 69, Copyright (C) year name of author
+ Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, the commands you use may
+be called something other than `show w' and `show c'; they could even be
+mouse-clicks or menu items--whatever suits your program.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the program, if
+necessary. Here is a sample; alter the names:
+
+ Yoyodyne, Inc., hereby disclaims all copyright interest in the program
+ `Gnomovision' (which makes passes at compilers) written by James Hacker.
+
+ , 1 April 1989
+ Moe Ghoul, President of Vice
+
+This General Public License does not permit incorporating your program into
+proprietary programs. If your program is a subroutine library, you may
+consider it more useful to permit linking proprietary applications with the
+library. If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..d25ef05
--- /dev/null
+++ b/README.md
@@ -0,0 +1,108 @@
+
+
+# WP Plugin Starter
+
+A clean start for a WordPress plugin: a settings screen, a Read Me tab, updates from GitHub and release scripts, ready for your features.
+
+WP Plugin Starter is what wpallstars plugins are made from. It has no features of its own: it holds the parts every plugin needs, built and tested in real plugins, so a new plugin starts with them working and spends its time on what makes it different.
+
+If this saves you time, headaches and costs, feel free to [buy me a coffee](https://buymeacoffee.com/marcusquinn), or whatever you like, to invest in making more things open-source.
+
+Version: {WPSTARTER_VERSION}
+
+## What you get
+
+- **A settings screen** (Settings → WP Plugin Starter) that features fill by declaring their settings: switches, numbers, text, lists, choices and Media Library pictures, saved instantly with no Save button, searchable, in tabs. With no features yet it shows one empty tab.
+- **A Read Me tab** that shows this file, banner included, so users read the same guide inside WordPress as on GitHub.
+- **Features as classes**: one file per feature, off by default, with settings, hooks, one-off imports from the plugins it replaces and clean uninstall.
+- **Replaced plugins**: a feature that does another plugin's job imports its settings once, waits while that plugin is active, and the Plugins screen suggests deactivating and deleting it.
+- **Updates from GitHub**: the shared wpallstars updater (`includes/github-updater/`). Sites get each GitHub release as a normal WordPress update. Every wpallstars plugin carries a copy and only the newest copy on a site runs, so they are all checked together, once.
+- **Two builds of each version**: the GitHub release, and a WordPress.org build without the updater, as WordPress.org requires.
+- **Scripts and CI**: lint (PHP 7.4, WordPress coding and security rules, PHPStan), a smoke test on a real WordPress, the release build, a preflight check of both zips, Plugin Check, a preview site, the banner build, and `scripts/sync-core.sh` to keep each plugin's shared parts the same as the starter's.
+- **Shared rules for people and AI**: `STANDARDS.md` (structure, code rules, releases, styling, testing), `DEVELOPMENT.md` (set-up and checks) and `RELEASING.md`, the same in every plugin made from the starter.
+
+## Start a plugin
+
+1. On GitHub, choose **Use this template** to make your repository, and clone it.
+2. Give it its names: `scripts/rename-plugin.sh --slug my-plugin --name "My Plugin" --prefix MyPlugin`. Add `--css mp` for a short CSS prefix and `--repo owner/repo` if it is not under wpallstars. Review with `git diff`, then commit.
+3. Replace this README, `readme.txt`, `changelog.txt` and `AGENTS.md` with your plugin's own, and its banner (`.wordpress-org/banner.svg`, then `scripts/build-banner.sh`).
+4. Add features: a class in `includes/features/` listed in `MyPlugin_Setup::FEATURES` (see Developers below and `STANDARDS.md`).
+5. Keep the shared parts up to date: change them in the starter first, then run `scripts/sync-core.sh` in each plugin (`--check` lists what differs).
+
+The easiest way to do all of this is with [aidevops](https://aidevops.sh): open the repository with it and describe the plugin you want. It reads `AGENTS.md` and `STANDARDS.md`, builds the features, tests them on a real site and runs the release checks.
+
+## Where to find it
+
+Go to **Settings → WP Plugin Starter**. The screen has two groups of tabs:
+
+- **Settings**: General, empty until features add settings. Changes save instantly; there is no Save button. **Search features** (next to the plugin name) finds settings on every tab.
+- **About**: this Read Me.
+
+**Report a problem**, at the top right of the screen, opens the plugin’s [GitHub issues](https://github.com/wpallstars/wp-plugin-starter-template-for-ai-coding/issues) in a new tab. Say what you did, what you expected and what happened, with the versions of WordPress, PHP and WP Plugin Starter. Leave out passwords, licence keys and personal data, since issues are public. **Buy me a coffee**, next to it, opens the maker’s [Buy Me a Coffee](https://buymeacoffee.com/marcusquinn) page in a new tab.
+
+## Requirements
+
+- WordPress 6.2 or later
+- PHP 7.4 or later
+
+## Updates and releases
+
+There are two builds of each version:
+
+- **GitHub release** (`wp-plugin-starter-template-X.Y.Z.zip` on the repository’s Releases page): everything, including the shared updater in `includes/github-updater/`, so sites get each release as a normal update.
+- **WordPress.org**: the same files without the updater (listed in `.distignore-wporg`) and without the `GitHub Plugin URI`, `Primary Branch` and `Release Asset` header lines, because plugins hosted there may not install or update code from elsewhere.
+
+The updater waits while Git Updater is active, so the two never both update a plugin.
+
+Releasing on GitHub:
+
+1. Merge the version change (`Version:` and `WPSTARTER_VERSION` in `wp-plugin-starter-template.php`, `Stable tag:` in `readme.txt`) to `main`.
+2. Straight away, tag that commit `vX.Y.Z` and publish a GitHub release with `wp-plugin-starter-template-X.Y.Z.zip` attached. `scripts/build-release.sh --ref vX.Y.Z` builds it (and the WordPress.org zip) from the tag with `.distignore` applied, everything inside a `wp-plugin-starter-template/` folder; `scripts/preflight-release.sh` and `scripts/plugin-check.sh` check them first. Sites pick the latest release whose tag is a plain version number and the asset whose name starts with `wp-plugin-starter-template`, so never attach the WordPress.org zip. Full steps: `RELEASING.md`.
+3. Sites offer the update when they next check (within 12 hours, or at once with **Check again** on the Updates screen).
+
+Mark test builds as pre-releases on GitHub (or tag them with letters, such as `v1.2.0-rc1`): sites never offer those. Do not add an `Update URI` header: WordPress.org rejects it, and Plugin Check reports it as an updater.
+
+## Developers
+
+A feature is a class in `includes/features/class-wpstarter-{name}.php` that extends `WPStarter_Feature`, listed in `WPStarter_Setup::FEATURES`. It declares its settings in `settings()`, adds its hooks in `boot()` (returning early unless `self::enabled()`), and can import another plugin’s settings once in `migrate()` with `self::import_setting()`. Anything only this plugin needs goes in `WPStarter_Setup` (`includes/class-wpstarter-setup.php`): features, settings tabs, header links, settings version and its own helpers. The other files in `includes/` and `admin/` are the starter’s core files: `STANDARDS.md` → Structure.
+
+Filters:
+
+- `wpstarter_features`: register a feature class that extends `WPStarter_Feature`.
+- `wpstarter_settings_schema`: add or change settings. Each entry sets `type` (bool, int, text, url, lines, domains, select, multi, times or media, a picture from the Media Library stored as its attachment ID), `default`, `label`, `description` and either `tab` or `parent`; select and multi also take `options` (an array or a callable), and multi takes `open` to keep saved values that are not currently registered. A child setting can take `hidden` (true): it is not shown or searched, for wiring that code sets. `reload` (true) makes the saved message ask to reload the page, for changes that show only after a page load. `replaces` (slug => name) shows which plugin a feature replaces. Settings render and save automatically.
+- `wpstarter_admin_tabs`: add or reorder admin tabs. Each tab sets `label`, `group` (settings, discover or about), a `render` callback and an optional `capability`; tabs the current user lacks the capability for are hidden.
+- `wpstarter_admin_script_deps` and `wpstarter_admin_script_data`: the settings screen script’s dependencies and the data it reads as `wpstarterAdmin` (both with the active tab).
+- `wpstarter_can_change_settings`: return false to stop the current user changing WP Plugin Starter’s settings (on top of `manage_options`).
+- `wpstarter_replaced_plugin_extras`: what a replaced plugin does on this site that WP Plugin Starter does not (plain names, plugin folder). The Plugins screen names them instead of saying the plugin can go.
+- `wpstarter_stored_active_plugins` and `wpstarter_plugins_skipped`: for code that skips plugins on some requests, the active plugin files as stored and whether some are skipped on this request, so features still count those plugins as active.
+- `wpallstars_github_plugins` (GitHub builds, shared updater): change which plugins update from GitHub releases (plugin file => `repo` as owner/repo, `asset_only`, `version`, `name`).
+- `wpallstars_github_token` (GitHub builds, shared updater): GitHub token for a repository (token, owner/repo), for private repositories; defaults to the `WPALLSTARS_GITHUB_TOKEN` constant.
+- `wpallstars_github_updater_enabled` and `wpallstars_github_updater_early` (GitHub builds, shared updater): whether it runs (false while Git Updater is active) and whether plugins also on WordPress.org take GitHub releases first.
+
+Actions:
+
+- `wpstarter_admin_enqueue`: enqueue the plugin’s own admin styles and scripts on its settings screen (active tab), depending on `wpstarter-admin`. Its script can use `wpstarterAdmin.api` (`post`, `speak`, `errorMessage`).
+- `wpstarter_setting_saved`: a setting was saved from the admin screen.
+- `wpstarter_setting_panel`: print status at the top of a setting’s options panel (setting key, schema entry); wrap it in `
`.
+- `wpstarter_settings_tab_after`: print a section after a settings tab’s cards (tab slug).
+
+Read a setting with `WPStarter_Settings::get( 'key' )`.
+
+## Uninstall
+
+Deleting the plugin removes its settings, its cached data, who hid lines of the Plugins screen notice about replaced plugins, and the cached GitHub releases.
+
+## Changelog
+
+### 1.0.0
+
+- A fresh start, made from the parts every wpallstars plugin needs, taken from plugins in daily use: the settings screen with search, the Read Me tab, features as classes, replaced plugins, the shared GitHub updater, the release and check scripts, CI and the shared rules (`STANDARDS.md`, `DEVELOPMENT.md`, `RELEASING.md`). Earlier versions of this starter are retired; start again from this one.
+- New: `scripts/rename-plugin.sh` gives a copy of the starter its own names, and `scripts/sync-core.sh` keeps a plugin's core files the same as the starter's (`--check` lists any that differ).
+
+## Built with AI
+
+WP Plugin Starter is built and maintained with [aidevops](https://aidevops.sh), the same developer's open-source AI harness for creating and managing anything online with AI, plugins like this one included. It is free on [GitHub](https://github.com/marcusquinn/aidevops).
+
+## License
+
+GPL-2.0-or-later.
diff --git a/RELEASING.md b/RELEASING.md
new file mode 100644
index 0000000..e9b4df5
--- /dev/null
+++ b/RELEASING.md
@@ -0,0 +1,130 @@
+# Releasing
+
+How every plugin made from the wpallstars starter plugin is released. This
+file is the same in each of them (names as placeholders: `STANDARDS.md`
+lists them, and the plugin's `AGENTS.md` gives its values). The plugin's own
+submission state and guideline review: `LAUNCH.md`.
+
+Releases on GitHub and the WordPress.org submission need the owner's say.
+None of the scripts below tags, publishes, uploads or commits anything.
+
+## Scripts
+
+| Script | What it does |
+|--------|--------------|
+| `scripts/build-release.sh [--ref REF] [--out DIR]` | Builds both zips from a Git ref (default `HEAD`) into `dist/` (gitignored), with `SHA256SUMS`. Files come from Git, never the working tree. |
+| `scripts/preflight-release.sh [--ref REF] [--strict] [--offline]` | Checks versions, headers, readme, both zips (layout, development files, PHP 7.4 and JS syntax, updater code), remote assets, presets and starter data where the plugin has them, and the Git tag. Errors stop a release; `--strict` also fails on warnings, for a WordPress.org submission. |
+| `scripts/plugin-check.sh [--ref REF] [--zip FILE]` | Runs Plugin Check on both zips in a disposable WordPress in Docker, then removes it. |
+
+The two builds of each version:
+
+| Zip | Contents | Goes to |
+|-----|----------|---------|
+| `{slug}-X.Y.Z.zip` | Files in Git, less `.distignore` | GitHub release asset |
+| `wordpress-org-{slug}-X.Y.Z.zip` | The same, less `.distignore-wporg` (the GitHub updater) and the `GitHub Plugin URI`, `Primary Branch` and `Release Asset` header lines | WordPress.org only |
+
+Sites install the release asset whose name starts with `{slug}` (the shared
+GitHub updater, and Git Updater where it is still active), so the
+WordPress.org zip is named differently and must never be attached to a
+GitHub release.
+
+Plugin Check reports the GitHub updater as an updater
+(`plugin_updater_detected`, `update_modification_detected`, and
+`OffloadedContent` for its raw.githubusercontent.com address) and its shared
+`wpallstars_` names as unprefixed, in the GitHub zip; `scripts/plugin-check.sh`
+lists those as expected there and fails on the updater findings in the
+WordPress.org zip.
+
+## GitHub release
+
+1. In a pull request, set the version in `{slug}.php` (`Version:` and
+ `{PREFIX}_VERSION`) and `readme.txt` (`Stable tag:`), rename the
+ changelog's Unreleased section to the version in `readme.txt`,
+ `changelog.txt` and `README.md`, and add an upgrade notice if people need
+ to act. `readme.txt` keeps only the newest version, in short, and must stay
+ under 10 KB; `changelog.txt` keeps every version in full.
+ Numbers only (`1.2.3`): sites still on Git Updater offer a `Version:` on
+ `main` to every site, and the shared updater skips tags with letters.
+2. On the pull request's branch: `scripts/preflight-release.sh` (no errors) and
+ `scripts/plugin-check.sh` (no errors).
+3. Merge, then straight away:
+
+ ```bash
+ git fetch origin
+ scripts/preflight-release.sh --ref origin/main # no errors before tagging
+ git tag -a vX.Y.Z origin/main -m "{Name} X.Y.Z"
+ scripts/build-release.sh --ref vX.Y.Z
+ git push origin vX.Y.Z
+ gh release create vX.Y.Z dist/{slug}-X.Y.Z.zip --title "{Name} X.Y.Z" --notes-file
+ ```
+
+ Sites with the shared updater see the release when they next check.
+ Sites still on Git Updater are offered the `Version:` on `main` before the
+ release exists, so do this in the same sitting as the merge.
+4. Check the release has exactly one asset, `{slug}-X.Y.Z.zip`, and on a
+ site with the previous version that **Check again** on the Updates screen
+ shows the update and that it installs.
+
+Sites read the repository without signing in, so it must be public for sites
+to get updates. While it is private, a test site can use a read-only token
+in `WPALLSTARS_GITHUB_TOKEN` (`wp-config.php`; it serves every plugin with
+the shared updater). Making it public needs the owner's say; do the quality
+sweep in `DEVELOPMENT.md` → At public launch in the same step.
+
+## WordPress.org
+
+Guidelines: [Detailed Plugin Guidelines](https://developer.wordpress.org/plugins/wordpress-org/detailed-plugin-guidelines/),
+[Planning, submitting and maintaining](https://developer.wordpress.org/plugins/wordpress-org/planning-submitting-and-maintaining-plugins/),
+[Plugin readmes](https://developer.wordpress.org/plugins/wordpress-org/how-your-readme-txt-works/),
+[Plugin assets](https://developer.wordpress.org/plugins/wordpress-org/plugin-assets/),
+[Using Subversion](https://developer.wordpress.org/plugins/wordpress-org/how-to-use-subversion/).
+
+Before submitting, work through the plugin's `LAUNCH.md`: what
+`scripts/preflight-release.sh --strict` still reports, and a review of the
+code against each guideline (no code from elsewhere, external services,
+scripts and styles shipped with the plugin, links, admin notices, defaults,
+files outside the plugin folder, uninstall, GPL, name and trademarks, a
+complete plugin).
+
+### Submitting
+
+1. `scripts/build-release.sh --ref vX.Y.Z` (a version already released on
+ GitHub), then `scripts/preflight-release.sh --ref vX.Y.Z --strict` passes,
+ or every warning is accepted.
+2. `scripts/plugin-check.sh --ref vX.Y.Z` reports no errors; read every
+ warning.
+3. Test the WordPress.org zip on a clean site (latest WordPress and 6.2 with
+ PHP 7.4), with `WP_DEBUG` on: activate, turn each feature on and off,
+ deactivate, delete. `debug.log` stays empty.
+4. Signed in as the owner's WordPress.org account, upload
+ `dist/wordpress-org-{slug}-X.Y.Z.zip` at
+ [Add your plugin](https://wordpress.org/plugins/developers/add/), and ask
+ for the `{slug}` slug in the notes.
+5. Review takes about 1 to 10 business days. Reply to the reviewer's email
+ from the same account; fix issues in the repository, not in a copy.
+
+### After approval (Subversion)
+
+The SVN repository is `https://plugins.svn.wordpress.org/{slug}/` (or the
+slug given). It is for releases only, not development. The SVN password is
+separate from the account password and is set on the WordPress.org profile
+(see Using Subversion). Install Subversion first (`brew install subversion`).
+
+1. Check out the empty repository: `svn co https://plugins.svn.wordpress.org/{slug}/ svn-{slug}`.
+2. Unzip the WordPress.org build's `{slug}/` folder into `trunk/` (no zip
+ files in SVN), `svn add` new files, `svn rm` removed ones.
+3. Copy trunk to the tag: `svn cp trunk tags/X.Y.Z`. `Stable tag:` in
+ `trunk/readme.txt` and `tags/X.Y.Z/readme.txt` must be `X.Y.Z`; never
+ `trunk`.
+4. Put the banners, icons and screenshots in `assets/`.
+5. `svn ci -m "Release X.Y.Z"`, then check the plugin page and download.
+6. Consider release confirmation emails (Plugin Handbook → Release
+ Confirmation Emails), so a release goes out only after it is confirmed.
+
+Each later WordPress.org release: build from the tag that is already on GitHub,
+`--strict` preflight, Plugin Check, then steps 2 to 5. Readme-only changes
+(such as raising Tested up to) go to trunk and the current tag.
+
+Once listed, sites with the GitHub build update from WordPress.org, unless
+the plugin returns true from `wpallstars_github_updater_early` (a setting
+such as Early updates from GitHub), which keeps them on GitHub releases.
diff --git a/SECURITY.md b/SECURITY.md
new file mode 100644
index 0000000..75cc438
--- /dev/null
+++ b/SECURITY.md
@@ -0,0 +1,37 @@
+# Security policy
+
+## Supported versions
+
+Security fixes go into the latest release only. Update to it before
+reporting, and check that the problem is still there.
+
+## Reporting a vulnerability
+
+Please do not open a public issue, pull request or discussion for a
+security problem.
+
+Report it privately on GitHub: open the repository's **Security** tab and
+choose **Report a vulnerability**. Include:
+
+- the WP Plugin Starter version, and whether it came from GitHub or
+ WordPress.org;
+- the WordPress and PHP versions;
+- the feature involved and the settings needed;
+- steps to reproduce it, and what an attacker gains (for example, which
+ role they need and what they can then read or change).
+
+We reply within 30 days, agree a fix and a date with you, and credit you in
+the changelog unless you would rather not be named. If it is urgent (for
+example, it is already being used against sites), report it on GitHub and
+also email security@evergreen.je with a link to the report. Follow-up
+emails are welcome.
+
+## Scope
+
+In scope: the code in this repository, as installed from a GitHub release
+or WordPress.org.
+
+Out of scope: other plugins, themes and services that WP Plugin Starter
+recommends, links to or works with (report those to their makers), and
+problems that need an administrator account, since administrators can
+already run code on their site.
diff --git a/STANDARDS.md b/STANDARDS.md
new file mode 100644
index 0000000..7df23c3
--- /dev/null
+++ b/STANDARDS.md
@@ -0,0 +1,278 @@
+# Plugin standards
+
+Rules every plugin made from the wpallstars starter plugin follows. This file
+is the same in each of them: the starter holds the master copy, so a lesson
+learned in one plugin goes into the starter's copy and then to every plugin
+(`scripts/sync-core.sh` shows the differences). Never put rules for one
+plugin here; they go in its `AGENTS.md`.
+
+Names below are placeholders. Each plugin's `AGENTS.md` gives its values:
+
+| Placeholder | Meaning | Example |
+|---|---|---|
+| `{slug}` | Folder, main file and text domain | `my-plugin` |
+| `{prefix}` | Option, hook, function and file prefix: `{PREFIX}` in lower case | `myplugin` |
+| `{Prefix}` | Class prefix (`@package`) | `MyPlugin` |
+| `{PREFIX}` | Constant prefix | `MYPLUGIN` |
+| `{Name}` | Plugin name | My Plugin |
+| `{css}` | CSS class and data attribute prefix (`{css}-card`, `data-{css}-setting`) | `mp` |
+
+Minimums: **WordPress 6.2, PHP 7.4** (`readme.txt`, plugin header).
+How changes are made and checked: `DEVELOPMENT.md`. Releases: `RELEASING.md`.
+
+## Structure
+
+- Core files are listed in `scripts/core-files.txt`: the feature registry
+ (`includes/class-{prefix}.php`), the base feature
+ (`includes/class-{prefix}-feature.php`), the settings store
+ (`includes/class-{prefix}-settings.php`), the admin screen and Read Me tab
+ (`admin/`), the shared GitHub updater (`includes/github-updater/`), the
+ scripts, the CI workflow and tool configuration, and the shared docs (this
+ file, `DEVELOPMENT.md`, `RELEASING.md`, `CONTRIBUTING.md`, `SECURITY.md`).
+ A plugin's copy differs from the starter's only in the names above. They
+ hold no code for one plugin: they read `{Prefix}_Setup`
+ (`includes/class-{prefix}-setup.php`: features, settings tabs, header
+ links, settings version and history, the plugin's own helpers and admin
+ parts) or use hooks (`{prefix}_admin_tabs` for tabs,
+ `{prefix}_admin_enqueue` for the plugin's own admin CSS and JS,
+ `scripts/preflight-plugin.sh` for its own release checks). Anything only
+ one plugin needs goes there, in a feature, or in its own file loaded from
+ there. To change a core file, change the starter first, then run
+ `scripts/sync-core.sh` in each plugin; `scripts/sync-core.sh --check` lists
+ core files that differ. A new plugin starts as a copy of the starter
+ renamed with `scripts/rename-plugin.sh`.
+- One class per feature in `includes/features/`, extending `{Prefix}_Feature`,
+ registered in `{Prefix}_Setup::FEATURES`. Features some builds leave out
+ go in `{Prefix}_Setup::OPTIONAL_FEATURES` and load only when present.
+- `settings()` declares the schema; the admin screen renders, searches and
+ saves it with no extra code. Field types and keys: `README.md` → Developers.
+- `boot()` returns early unless `self::enabled()`. Features are **off by
+ default**; the plugin's `AGENTS.md` lists any the owner asked to be on.
+ Turning another feature on by default needs the owner's say.
+- A feature that replaces another plugin sets `'replaces' => array(slug => name)`
+ and imports that plugin's settings in `migrate()` with
+ `self::import_setting()` (fills only unset keys). It never writes or
+ deletes the other plugin's options. While that plugin is active the
+ feature waits, and the Plugins screen suggests deactivating and deleting
+ it (`admin/includes/class-replaced-plugins.php`) with no extra code.
+- Migrations run once per `{Prefix}_Setup::DB_VERSION`. After a release, a
+ new or changed import needs a version bump and a line in its docblock.
+- New options, post meta, user meta, transients, cron hooks and files must be
+ removed in `uninstall.php`.
+- `README.md` is also the plugin's Read Me tab
+ (`admin/includes/class-readme-manager.php`), which renders headings, lists,
+ tables, bold, italic, inline code, links (http(s) and `#heading` links,
+ with GitHub-style heading IDs) and images from the plugin folder on a line
+ of their own (``). Use only that Markdown,
+ or extend the renderer in the same change.
+- Update `README.md` (feature section, hooks, changelog), `changelog.txt`
+ (the user-facing changelog entry) and `readme.txt` in the same change.
+ `readme.txt` must stay under 10 KB for WordPress.org: one short line per
+ feature, every service the plugin contacts under External services, and
+ only the newest version's changelog, in short. Details go in `README.md`.
+- `.distignore` lists files kept out of the release zip. Add new
+ development-only files there (the preflight fails when a known one gets in),
+ then check the build with Plugin Check.
+
+## Code rules
+
+- PHP 7.4 syntax and WordPress 6.2 APIs. Guard newer core APIs with
+ `function_exists()` or `method_exists()`.
+- Capability and nonce checks on every admin action and AJAX handler; escape on
+ output; sanitise through the schema.
+- Prefix everything global with `{prefix}_`, `{Prefix}_` or `{PREFIX}_`. The
+ shared GitHub updater is the one exception: its `wpallstars_` names are the
+ same in every plugin, so that one copy can stand in for the others.
+- Admin copy: short, plain words, sentence case, no jargon.
+- Decide for the user. Within a feature, the plugin makes the choices
+ (which plugins, screens or items it applies to) from what it can detect.
+ Settings are there to bypass something that causes a problem, not choices
+ people need to understand first. A new setting must earn its place;
+ prefer detecting the right behaviour plus a short bypass list.
+- Site owner in control, performance first: the owner decides what their site
+ sends, contacts and shows. Calls to outside services are opt-in where they
+ are not the point of the feature, made only as often and for as long as
+ needed (cache answers, never on every page load), and never block a
+ visitor's page when they can run later. Features that rein in other
+ plugins hand the choice to the owner instead of deciding for them.
+ WordPress update checks and downloads are the exception: leave them alone
+ (next rule).
+- Do not change WordPress update behaviour (update transients, `auto_update_*`
+ filters, update checks) outside the shared GitHub updater. Plugin Check
+ reports `plugin_updater_detected` as an error, and WordPress.org asks plugins
+ not to interfere with the updater.
+- Leave no PHP errors, warnings, notices or deprecations behind. Fix any that
+ the plugin causes as you find them, in the same change when it is small,
+ or as a tracked issue. That includes ones in other plugins that only happen
+ because of this one. Messages that other plugins cause on their own are
+ theirs: mention them, do not hide them.
+
+## Updates from GitHub
+
+The one exception to the update rule, at the owner's request (for speed,
+reliability and site owners' control, and because the plugins are released
+on GitHub first): the shared GitHub updater in `includes/github-updater/`.
+It replaces Git Updater.
+
+- Every plugin made from the starter carries a copy. Each copy registers its
+ version from `load.php` when its plugin loads; on `plugins_loaded` only the
+ newest copy loads, once, and serves every installed plugin with a
+ `GitHub Plugin URI` header. One update check covers them all.
+- It adds GitHub releases of those plugins to core's own update check and
+ `plugins_api`, and leaves the download, install, auto-updates and rollback
+ to core. It only adds entries for those plugins; it never removes or blocks
+ other updates.
+- It is the same in every plugin apart from its text domain and `@package`.
+ Change it in the starter, raise the version in its `load.php`, and copy it
+ to each plugin. Plugins change what it does only through its filters
+ (`wpallstars_github_updater_enabled`, `wpallstars_github_updater_early`,
+ `wpallstars_github_plugins`, `wpallstars_github_token`), never by calling
+ its class: another plugin's copy may be the one that runs.
+- Keep anything that installs or updates code from outside WordPress.org in
+ that folder (and in a feature listed in `.distignore-wporg`, when a plugin
+ has a setting for it), because the WordPress.org build leaves them out.
+- Never add an `Update URI` header. Tokens for private repositories come only
+ from `wp-config.php` (`WPALLSTARS_GITHUB_TOKEN`) or the filter, go only to
+ api.github.com and are never stored.
+
+## Releases
+
+GitHub releases are the early channel; WordPress.org gets settled versions.
+Sites install the latest GitHub release whose tag is a plain version and the
+asset whose name starts with the plugin folder (the shared updater; Git
+Updater, where still active, reads `Version:` on `main` instead), so:
+
+- Publish the GitHub release (tag `vX.Y.Z`, asset `{slug}-X.Y.Z.zip` with a
+ `{slug}/` folder, built with `.distignore`) straight after the version
+ change reaches `main`.
+- Never put a pre-release version (`-beta1`, `-rc1`) in `Version:` on `main`;
+ mark test releases as pre-releases on GitHub.
+- The WordPress.org build is the release build without the files in
+ `.distignore-wporg` (the GitHub updater) and the `GitHub Plugin URI`,
+ `Primary Branch` and `Release Asset` header lines. Its zip is named
+ `wordpress-org-{slug}-X.Y.Z.zip` so no updater picks it; never attach it
+ to a GitHub release.
+- Any plugin released on GitHub (made from the starter or not) follows the
+ same pattern: a `GitHub Plugin URI: owner/repo` header (and
+ `Release Asset: true`), plain version tags, and a `{folder}-X.Y.Z.zip`
+ asset with a `{folder}/` inside.
+- Build both zips with `scripts/build-release.sh`, check them with
+ `scripts/preflight-release.sh` and `scripts/plugin-check.sh`. None of them
+ tags, publishes or uploads anything.
+- Releasing and submitting to WordPress.org need the owner's say. A private
+ repository cannot be read by sites without a token
+ (`WPALLSTARS_GITHUB_TOKEN`).
+
+Details: `RELEASING.md`; the plugin's own submission state: `LAUNCH.md`.
+
+## Front-end styling and dark mode
+
+Block, shortcode and other front-end styles must work with the Kadence Pro
+dark mode switcher (and themes that switch palettes the same way).
+
+- How it switches: Kadence adds `color-switch-dark` or `color-switch-light` to
+ ``. The dark class sets `color-scheme: dark` and redefines
+ `--global-palette1`…`15` and `--wp--preset--color--theme-palette-N` **on
+ ``**. `` stays `color-scheme: light`. Palette 3 is the strongest
+ text and palette 9 the page background in light mode; dark mode swaps them.
+- Use `currentColor`, `inherit`, translucent neutrals (for example
+ `rgba(127, 127, 127, 0.12)`) or palette variables, never fixed light or dark
+ colours for text, backgrounds or borders.
+- Do not use `@media (prefers-color-scheme)` to follow the site: it tracks the
+ visitor's system, not the switcher.
+- Do not define custom properties on `:root` from palette variables; they
+ resolve above `` and keep the light values. Read palette variables
+ where they are used, or define derived ones on the block.
+- Preset references (`var:preset|color|theme-palette3`) become CSS variables
+ with kebab-cased slugs, as core does: `--wp--preset--color--theme-palette-3`.
+ Use `_wp_to_kebab_case()` in PHP and the same rule in editor JS (a hyphen
+ between letters and digits, lower case).
+- Embedded pages (iframes) do not follow the switcher: they see the visitor's
+ system setting, and the browser paints their own background behind them, so
+ they stay readable in both modes. Do not make iframes transparent or tint them.
+- Test light and dark with the Kadence theme by toggling the body class (see
+ Testing, step 5).
+
+## Testing
+
+CI (`.github/workflows/ci.yml`) runs the code checks, the release preflight,
+Plugin Check and a smoke test on every pull request; details and local
+commands: `DEVELOPMENT.md`.
+
+While a repository is private, CI and review apps only advise: nothing is
+required to merge, for speed. Fix failures your change causes before merging;
+open an issue for any other failure and merge anyway. Do not turn on branch
+protection, required checks or paid reviewers. At public launch (owner's say),
+run the full sweep in `DEVELOPMENT.md` → At public launch, which makes the
+checks required.
+
+The checks catch errors, not wrong behaviour, so also verify on real
+WordPress:
+
+1. `scripts/lint.sh` (syntax, ShellCheck, PHPCS, PHPStan; run
+ `composer install` once). Fix findings in the code; never grow
+ `phpstan-baseline.neon` or add a `phpcs:ignore` without a reason.
+2. The user reviews on one local test site per plugin, shared by every
+ session and worktree. It shows a **combined preview**: `origin/main` plus
+ every open pull request from the repository, merged together. Update it
+ only with the script, from any worktree:
+
+ ```bash
+ scripts/preview-site.sh # the first run on a clone takes the site: scripts/preview-site.sh ""
+ scripts/preview-site.sh --dry-run # report what would be included, copy nothing
+ ```
+
+ It fetches `origin`, merges each open PR's branch onto `origin/main` in PR
+ order without touching any checkout, leaves out branches that conflict
+ (and lists them), copies the result with `.distignore` applied (exactly
+ what a release build contains), and writes
+ `/wp-content/{slug}-synced-from.txt` listing what is included.
+ A lock stops two runs at once. Because every run includes everyone's
+ pushed work, no session hides another's.
+
+ - **Never** `rsync` your worktree into the shared site: it hides every
+ other session's work until the next run. Do not use Git hooks either:
+ they are shared by every worktree and would deploy the wrong checkout.
+ - Only pushed work with an open PR is included. Before asking the user to
+ look at unmerged work, push the branch and open a draft PR, then run the
+ script. It says so if the branch you run it from is missing or has
+ commits that are not pushed.
+ - After merging a PR, run the script again, then check that the stamp
+ lists your merge in `main` before telling the user to look.
+ - Conflicts only in `changelog.txt`, `readme.txt` or `README.md` do not
+ leave a branch out: every PR adds lines at the top of the same
+ changelogs, so each merge to `main` would otherwise drop every other
+ open PR. The preview keeps both sides' lines there and says so in the
+ stamp; still merge `origin/main` into your branch before it merges.
+ - If your branch is left out because it conflicts in other files, merge
+ `origin/main` into it (or wait for the other PR), push and run the
+ script again. Tell the user which PRs are left out.
+ - To check your branch on its own, or for checks the user will not look
+ at, use a throwaway site of your own (step 4's Docker image on a free
+ port), which no one else overwrites:
+ `rsync -a --delete --delete-excluded --exclude-from=.distignore ./ "/wp-content/plugins/{slug}/"`.
+ - Size every test site, shared or throwaway, as `DEVELOPMENT.md` → Test
+ site resources says. PHP's defaults fill up and the slowdowns look like
+ bugs.
+
+3. Exercise the changed feature through the admin UI or HTTP, and check
+ the debug log for new messages mentioning `{slug}` or a plugin it affects
+ (`wp-content/debug.log`, or the file Debug Log Manager writes in
+ `wp-content/uploads/debug-log-manager/` when it is active). For settings
+ imports, seed the replaced plugin's options and delete `{prefix}_options`
+ and `{prefix}_db_version` while the plugin is inactive, then activate it.
+4. For changes that touch core APIs, also smoke-test on WordPress 6.2 with
+ PHP 7.4 (`scripts/smoke-test.sh --wp 6.2 --php 7.4`, or the
+ `wordpress:php7.4-apache` Docker image with
+ `wp core download --version=6.2 --force`).
+5. For front-end styling, view the page with the Kadence theme in light and
+ dark mode. Without Kadence Pro, simulate the switcher: print a
+ `body.color-switch-dark { color-scheme: dark; --global-palette1: …; }`
+ rule with a dark palette (palette 3 light, palette 9 dark, and the matching
+ `--wp--preset--color--theme-palette-N: var(--global-paletteN)` lines), then
+ swap the body class between `color-switch-light` and `color-switch-dark`.
+ Check text, backgrounds, borders and palette colours chosen in block
+ settings in both.
+
+Note: since WordPress 5.6, posts restored from the Bin become drafts. Republish
+test posts after bulk-trash tests.
diff --git a/admin/css/wpstarter-admin.css b/admin/css/wpstarter-admin.css
new file mode 100644
index 0000000..2fdada1
--- /dev/null
+++ b/admin/css/wpstarter-admin.css
@@ -0,0 +1,848 @@
+/**
+ * WP Plugin Starter admin screen.
+ *
+ * Scoped to .wps-wrap. Uses core admin tokens so the screen follows the
+ * active admin colour scheme; core buttons, notices and plugin cards keep
+ * their native styles. Logical properties keep RTL working without a
+ * separate stylesheet.
+ */
+
+.wps-wrap {
+ --wps-accent: var(--wp-admin-theme-color, #2271b1);
+ --wps-accent-dark: var(--wp-admin-theme-color-darker-10, #135e96);
+ --wps-text: #1d2327;
+ --wps-muted: #646970;
+ --wps-border: #dcdcde;
+ --wps-surface: #fff;
+ --wps-subtle: #f6f7f7;
+ --wps-success: #008a20;
+ --wps-error: #d63638;
+ --wps-radius: 8px;
+ --wps-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
+ --wps-focus: 0 0 0 2px var(--wps-surface), 0 0 0 4px var(--wps-accent);
+
+ margin-block-start: 0;
+ margin-inline-start: -20px;
+ margin-inline-end: 0;
+ color: var(--wps-text);
+ /* Start below the Screen Options / Notices tabs, so they never narrow the header. */
+ clear: both;
+}
+
+.wps-wrap *,
+.wps-wrap *::before,
+.wps-wrap *::after {
+ box-sizing: border-box;
+}
+
+/* Header ------------------------------------------------------------- */
+
+.wps-header {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ gap: 12px 24px;
+ padding: 14px 24px;
+ background: var(--wps-surface);
+ border-block-end: 1px solid var(--wps-border);
+}
+
+.wps-header__brand {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+}
+
+.wps-header__logo {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 32px;
+ height: 32px;
+ font-size: 20px;
+ color: #fff;
+ background: var(--wps-accent);
+ border-radius: 6px;
+}
+
+.wps-wrap .wps-header__title {
+ margin: 0;
+ padding: 0;
+ font-size: 20px;
+ font-weight: 600;
+ line-height: 1.3;
+}
+
+.wps-header__actions {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ gap: 8px;
+ margin-inline-start: auto;
+}
+
+.wps-wrap .wps-header__support {
+ display: inline-flex;
+ align-items: center;
+ gap: 4px;
+}
+
+/* line-height: 1 stops the icon taking the button's taller line height,
+ which draws the glyph below the middle of its box. */
+.wps-wrap .wps-header__support .dashicons {
+ width: 18px;
+ height: 18px;
+ font-size: 18px;
+ line-height: 1;
+}
+
+/* Search ------------------------------------------------------------- */
+
+/* Stretch, so the field always matches the button's height, whatever
+ size the admin colour scheme or WordPress version gives buttons. */
+.wps-search {
+ display: flex;
+ align-items: stretch;
+ gap: 6px;
+}
+
+.wps-search__field {
+ position: relative;
+ display: flex;
+ align-items: stretch;
+}
+
+.wps-wrap .wps-search__icon {
+ position: absolute;
+ inset-block: 0;
+ inset-inline-start: 9px;
+ margin-block: auto;
+ color: var(--wps-muted);
+ pointer-events: none;
+}
+
+.wps-wrap .wps-search__input {
+ width: 260px;
+ max-width: 100%;
+ height: auto;
+ min-height: 30px;
+ margin: 0;
+ padding-block: 0;
+ padding-inline: 34px 10px;
+ font-size: 14px;
+ line-height: normal;
+ color: var(--wps-text);
+ background: var(--wps-surface);
+ /* Border and corners stay core's, so they match core buttons in each version. */
+ box-shadow: none;
+ transition: border-color 0.1s ease-in-out, box-shadow 0.1s ease-in-out;
+}
+
+.wps-wrap .wps-search__input::placeholder {
+ color: var(--wps-muted);
+ opacity: 1;
+}
+
+.wps-wrap .wps-search__input:hover {
+ border-color: var(--wps-accent);
+}
+
+.wps-wrap .wps-search__input:focus {
+ border-color: var(--wps-accent);
+ box-shadow: 0 0 0 1px var(--wps-accent);
+ outline: 2px solid transparent;
+}
+
+.wps-wrap .wps-search__field:focus-within .wps-search__icon {
+ color: var(--wps-accent);
+}
+
+.wps-wrap .wps-search__button {
+ margin: 0;
+}
+
+.wps-wrap .wps-search__group {
+ margin: 24px 0 8px;
+ font-size: 14px;
+ font-weight: 600;
+}
+
+.wps-wrap .wps-search__group:first-of-type {
+ margin-block-start: 8px;
+}
+
+/* Navigation --------------------------------------------------------- */
+
+.wps-nav {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: stretch;
+ padding-inline: 12px;
+ background: var(--wps-surface);
+ border-block-end: 1px solid var(--wps-border);
+ overflow-x: auto;
+}
+
+.wps-nav__group {
+ display: flex;
+ align-items: stretch;
+}
+
+.wps-nav__group + .wps-nav__group {
+ margin-inline-start: 8px;
+ padding-inline-start: 8px;
+ border-inline-start: 1px solid var(--wps-border);
+}
+
+.wps-nav__tab {
+ display: inline-flex;
+ align-items: center;
+ padding: 14px 12px 11px;
+ font-size: 14px;
+ font-weight: 500;
+ color: var(--wps-muted);
+ text-decoration: none;
+ white-space: nowrap;
+ border-block-end: 3px solid transparent;
+ transition: color 0.1s ease, border-color 0.1s ease;
+}
+
+.wps-nav__tab:hover {
+ color: var(--wps-text);
+}
+
+.wps-nav__tab:focus {
+ outline: none;
+ box-shadow: none;
+}
+
+.wps-nav__tab:focus-visible {
+ outline: 2px solid var(--wps-accent);
+ outline-offset: -4px;
+ border-radius: 4px;
+}
+
+.wps-nav__tab.is-active {
+ color: var(--wps-text);
+ font-weight: 600;
+ border-block-end-color: var(--wps-accent);
+}
+
+/* Main --------------------------------------------------------------- */
+
+.wps-wrap > .notice,
+.wps-wrap > div.updated,
+.wps-wrap > div.error {
+ margin: 16px 24px 0;
+}
+
+.wps-main {
+ max-width: 1200px;
+ padding: 24px;
+}
+
+.wps-section {
+ max-width: 860px;
+}
+
+.wps-section__intro {
+ margin-block-end: 16px;
+}
+
+.wps-wrap .wps-section__title {
+ margin: 0 0 4px;
+ font-size: 18px;
+ font-weight: 600;
+}
+
+.wps-section__desc,
+.wps-section__hint {
+ margin: 0;
+ color: var(--wps-muted);
+}
+
+.wps-section__hint {
+ margin-block-start: 4px;
+ font-size: 12px;
+}
+
+/* Cards -------------------------------------------------------------- */
+
+.wps-card {
+ background: var(--wps-surface);
+ border: 1px solid var(--wps-border);
+ border-radius: var(--wps-radius);
+ box-shadow: var(--wps-shadow);
+}
+
+.wps-cards {
+ display: grid;
+ gap: 12px;
+}
+
+/* A settings tab with nothing in it yet. */
+.wps-empty {
+ padding: 16px 20px;
+ color: var(--wps-muted);
+}
+
+.wps-empty p {
+ margin: 0;
+}
+
+.wps-badge {
+ display: inline-block;
+ padding: 2px 8px;
+ font-size: 11px;
+ font-weight: 500;
+ line-height: 1.6;
+ color: var(--wps-muted);
+ vertical-align: middle;
+ background: var(--wps-subtle);
+ border: 1px solid var(--wps-border);
+ border-radius: 999px;
+}
+
+.wps-badge--success {
+ color: #00450c;
+ background: #edfaef;
+ border-color: #b8e6bf;
+}
+
+/* Setting cards ------------------------------------------------------ */
+
+/* Clip the header hover and panel backgrounds to the rounded corners. */
+.wps-setting {
+ position: relative;
+ overflow: hidden;
+}
+
+/* "On" stripe drawn above the header/panel backgrounds so hover never hides it. */
+.wps-setting::before {
+ content: "";
+ position: absolute;
+ z-index: 1;
+ inset-block: 0;
+ inset-inline-start: 0;
+ width: 3px;
+ background: var(--wps-accent);
+ opacity: 0;
+ pointer-events: none;
+ transition: opacity 0.15s ease;
+}
+
+.wps-setting.is-on::before {
+ opacity: 1;
+}
+
+.wps-setting__header {
+ display: grid;
+ grid-template-columns: auto 1fr auto;
+ align-items: center;
+ gap: 16px;
+ padding: 16px 20px;
+}
+
+.wps-setting.has-panel .wps-setting__header {
+ cursor: pointer;
+}
+
+.wps-setting.has-panel .wps-setting__header:hover {
+ background: #fcfcfc;
+}
+
+.wps-setting__text {
+ min-width: 0;
+}
+
+/* Status sits beside the title so showing/hiding it never reflows the description. */
+.wps-setting__title-row {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ gap: 4px 10px;
+ min-height: 20px;
+}
+
+.wps-setting__title {
+ font-size: 14px;
+ font-weight: 600;
+}
+
+.wps-setting__desc {
+ margin: 2px 0 0;
+ color: var(--wps-muted);
+}
+
+.wps-replaces {
+ margin: 4px 0 0;
+ font-size: 12px;
+ color: var(--wps-muted);
+}
+
+.wps-replaces__active {
+ color: #996800;
+}
+
+.wps-wrap .wps-setting__expand {
+ display: inline-flex;
+ align-items: center;
+ gap: 4px;
+ padding: 6px 8px;
+ color: var(--wps-accent);
+ text-decoration: none;
+ border-radius: 4px;
+}
+
+.wps-wrap .wps-setting__expand:hover {
+ background: var(--wps-subtle);
+}
+
+.wps-wrap .wps-setting__expand:focus-visible {
+ outline: none;
+ box-shadow: var(--wps-focus);
+}
+
+.wps-setting__expand .dashicons {
+ transition: transform 0.15s ease;
+}
+
+.wps-setting__expand[aria-expanded="true"] .dashicons {
+ transform: rotate(180deg);
+}
+
+.wps-setting__panel {
+ padding: 4px 20px 8px;
+ border-block-start: 1px solid var(--wps-border);
+ background: #fcfcfc;
+}
+
+.wps-setting__panel[hidden] {
+ display: none;
+}
+
+/* Status a feature prints above its options (wpstarter_setting_panel). */
+.wps-panel-note {
+ padding: 12px 0;
+ border-block-end: 1px solid #f0f0f1;
+ color: var(--wps-muted);
+}
+
+.wps-panel-note p {
+ margin: 0 0 6px;
+}
+
+.wps-panel-note p:last-child {
+ margin-block-end: 0;
+}
+
+.wps-panel-note--warning {
+ color: #1d2327;
+}
+
+.wps-panel-note--warning p::before {
+ content: "\f534";
+ margin-inline-end: 6px;
+ font: 16px/1 dashicons;
+ vertical-align: -3px;
+ color: #dba617;
+}
+
+.wps-media__preview {
+ flex: none;
+ width: 64px;
+ height: 64px;
+ object-fit: contain;
+ background: repeating-conic-gradient(#f0f0f1 0% 25%, #fff 0% 50%) 0 0 / 16px 16px;
+ border: 1px solid var(--wps-border);
+ border-radius: 4px;
+}
+
+.wps-code {
+ max-width: 100%;
+ margin: 6px 0 0;
+ padding: 10px 12px;
+ overflow-x: auto;
+ font-size: 12px;
+ line-height: 1.5;
+ color: #1d2327;
+ background: #f6f7f7;
+ border: 1px solid var(--wps-border);
+ border-radius: 4px;
+ white-space: pre;
+}
+
+/* Fields ------------------------------------------------------------- */
+
+.wps-field {
+ display: grid;
+ grid-template-columns: 200px 1fr;
+ gap: 8px 24px;
+ padding: 16px 0;
+}
+
+.wps-field + .wps-field {
+ border-block-start: 1px solid #f0f0f1;
+}
+
+.wps-field__label {
+ padding-block-start: 6px;
+ font-weight: 600;
+}
+
+.wps-field__control .description {
+ margin-block-start: 6px;
+}
+
+.wps-field__unit {
+ color: var(--wps-muted);
+}
+
+.wps-field__control textarea.large-text {
+ max-width: 480px;
+}
+
+.wps-media {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px 12px;
+ align-items: center;
+}
+
+.wps-media__preview[hidden],
+.wps-media__remove[hidden] {
+ display: none;
+}
+
+.wps-checkboxes {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 6px 20px;
+ margin: 0;
+ padding: 6px 0 0;
+ border: 0;
+}
+
+.wps-checkbox {
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+}
+
+/* Long lists, such as every active plugin: columns that scroll. */
+.wps-checkboxes.is-long {
+ display: block;
+ max-height: 320px;
+ overflow-y: auto;
+ columns: 16em auto;
+ column-gap: 20px;
+ padding: 6px 8px;
+ border: 1px solid #dcdcde;
+ border-radius: 4px;
+}
+
+.wps-checkboxes.is-long .wps-checkbox {
+ display: flex;
+ align-items: flex-start;
+ margin-block-end: 6px;
+ break-inside: avoid;
+}
+
+.wps-checkboxes.is-long .wps-checkbox input {
+ margin-block-start: 2px;
+}
+
+.wps-checkboxes__all {
+ display: block;
+ margin-block-end: 4px;
+}
+
+.wps-tokens {
+ display: block;
+ margin-block-start: 6px;
+}
+
+.wps-wrap .wps-token {
+ margin: 2px 4px 2px 0;
+ padding: 0;
+ cursor: pointer;
+ background: none;
+ border: 0;
+}
+
+.wps-token code {
+ display: inline-block;
+ padding: 1px 6px;
+ font-size: 12px;
+ background: var(--wps-subtle);
+ border: 1px solid var(--wps-border);
+ border-radius: 4px;
+}
+
+.wps-token:hover code,
+.wps-token:focus-visible code {
+ color: var(--wps-accent);
+ border-color: var(--wps-accent);
+}
+
+.wps-token:focus {
+ outline: none;
+}
+
+/* Switch ------------------------------------------------------------- */
+
+.wps-switch {
+ position: relative;
+ display: inline-block;
+ flex: none;
+ width: 36px;
+ height: 20px;
+ vertical-align: middle;
+}
+
+.wps-wrap .wps-switch__input {
+ position: absolute;
+ inset: 0;
+ z-index: 1;
+ width: 100%;
+ height: 100%;
+ margin: 0;
+ cursor: pointer;
+ opacity: 0;
+}
+
+.wps-switch__track {
+ position: absolute;
+ inset: 0;
+ background: #fff;
+ border: 1px solid #1e1e1e;
+ border-radius: 999px;
+ transition: background-color 0.15s ease, border-color 0.15s ease;
+}
+
+.wps-switch__track::after {
+ content: "";
+ position: absolute;
+ inset-block-start: 3px;
+ inset-inline-start: 3px;
+ width: 12px;
+ height: 12px;
+ background: #1e1e1e;
+ border-radius: 50%;
+ transition: transform 0.15s ease, background-color 0.15s ease;
+}
+
+.wps-switch__input:checked + .wps-switch__track {
+ background: var(--wps-accent);
+ border-color: var(--wps-accent);
+}
+
+.wps-switch__input:checked + .wps-switch__track::after {
+ background: #fff;
+ transform: translateX(16px);
+}
+
+[dir="rtl"] .wps-switch__input:checked + .wps-switch__track::after {
+ transform: translateX(-16px);
+}
+
+.wps-switch__input:focus-visible + .wps-switch__track {
+ box-shadow: var(--wps-focus);
+}
+
+.wps-switch__input:disabled + .wps-switch__track {
+ opacity: 0.5;
+}
+
+/* Save status -------------------------------------------------------- */
+
+.wps-status {
+ display: inline-flex;
+ align-items: center;
+ gap: 4px;
+ min-height: 20px;
+ font-size: 12px;
+ color: var(--wps-muted);
+ white-space: nowrap;
+}
+
+.wps-field__control .wps-status {
+ margin-inline-start: 8px;
+}
+
+.wps-status.is-saving::before {
+ content: "";
+ width: 12px;
+ height: 12px;
+ border: 2px solid var(--wps-border);
+ border-block-start-color: var(--wps-accent);
+ border-radius: 50%;
+ animation: wps-spin 0.8s linear infinite;
+}
+
+.wps-status.is-saved {
+ color: var(--wps-success);
+}
+
+.wps-status.is-saved::before {
+ content: "\f147";
+ font: normal 18px/1 dashicons;
+}
+
+.wps-status.is-error {
+ color: var(--wps-error);
+ white-space: normal;
+}
+
+@keyframes wps-spin {
+ to {
+ transform: rotate(360deg);
+ }
+}
+
+/* Read Me ------------------------------------------------------------ */
+
+.wps-readme {
+ max-width: 820px;
+ padding: 24px 32px;
+ font-size: 14px;
+ line-height: 1.65;
+}
+
+.wps-readme-image {
+ margin: 0 0 16px;
+}
+
+.wps-readme-image img {
+ display: block;
+ max-width: 100%;
+ height: auto;
+ border-radius: var(--wps-radius);
+}
+
+/* The banner at the top of README.md fills the top of the card. */
+.wps-readme > .wps-readme-image:first-child {
+ margin: -24px -32px 24px;
+}
+
+.wps-readme > .wps-readme-image:first-child img {
+ width: 100%;
+ border-radius: calc(var(--wps-radius) - 1px) calc(var(--wps-radius) - 1px) 0 0;
+}
+
+.wps-wrap .wps-readme h2 {
+ margin: 0 0 8px;
+ font-size: 22px;
+}
+
+.wps-wrap .wps-readme h3 {
+ margin: 24px 0 8px;
+ padding-block-start: 16px;
+ font-size: 16px;
+ border-block-start: 1px solid #f0f0f1;
+}
+
+.wps-wrap .wps-readme h4 {
+ margin: 16px 0 6px;
+ font-size: 14px;
+}
+
+.wps-readme ul,
+.wps-readme ol {
+ margin: 8px 0 12px;
+ padding-inline-start: 22px;
+}
+
+.wps-readme ul {
+ list-style: disc;
+}
+
+.wps-readme ol {
+ list-style: decimal;
+}
+
+.wps-readme-table {
+ margin: 8px 0 16px;
+ overflow-x: auto;
+}
+
+.wps-readme table {
+ border-collapse: collapse;
+ font-size: 13px;
+ line-height: 1.5;
+}
+
+.wps-readme th,
+.wps-readme td {
+ padding: 6px 10px;
+ text-align: start;
+ vertical-align: top;
+ border: 1px solid rgba(127, 127, 127, 0.25);
+}
+
+.wps-readme th {
+ background: rgba(127, 127, 127, 0.08);
+}
+
+/* Responsive --------------------------------------------------------- */
+
+@media screen and (max-width: 782px) {
+ .wps-wrap {
+ margin-inline-start: -10px;
+ }
+
+ .wps-header,
+ .wps-main {
+ padding-inline: 12px;
+ }
+
+ .wps-nav {
+ flex-wrap: nowrap;
+ }
+
+ /* Brand and website link on the first row, search across the second. */
+ .wps-search {
+ order: 1;
+ flex: 1 1 100%;
+ min-width: 0;
+ }
+
+ .wps-search__field {
+ flex: 1 1 0;
+ min-width: 0;
+ }
+
+ .wps-wrap .wps-search__input {
+ flex: 1 1 0;
+ width: auto;
+ min-width: 0;
+ min-height: 40px;
+ font-size: 16px;
+ }
+
+ .wps-search .button {
+ flex-shrink: 0;
+ }
+
+ .wps-setting__header {
+ grid-template-columns: auto 1fr;
+ }
+
+ .wps-setting__expand {
+ grid-column: 2;
+ justify-self: start;
+ }
+
+ .wps-field {
+ grid-template-columns: 1fr;
+ }
+}
+
+@media (prefers-reduced-motion: reduce) {
+ .wps-wrap *,
+ .wps-wrap *::before,
+ .wps-wrap *::after {
+ transition: none !important;
+ animation: none !important;
+ }
+}
diff --git a/admin/data/readme.php b/admin/data/readme.php
new file mode 100644
index 0000000..80f48a1
--- /dev/null
+++ b/admin/data/readme.php
@@ -0,0 +1,30 @@
+ __('Read Me', 'wp-plugin-starter-template'),
+ 'content' => $content,
+ );
+}
diff --git a/admin/images/banner.svg b/admin/images/banner.svg
new file mode 100644
index 0000000..d76298e
--- /dev/null
+++ b/admin/images/banner.svg
@@ -0,0 +1,434 @@
+
+
+
+
diff --git a/admin/includes/class-admin-manager.php b/admin/includes/class-admin-manager.php
new file mode 100644
index 0000000..a5a890d
--- /dev/null
+++ b/admin/includes/class-admin-manager.php
@@ -0,0 +1,360 @@
+
+ */
+ public static function settings_tabs() {
+ return WPStarter_Setup::settings_tabs();
+ }
+
+ /**
+ * Registered tabs.
+ *
+ * @return array
+ */
+ public static function get_tabs() {
+ $tabs = array();
+
+ // Settings tabs without settings are hidden (they can be filled via
+ // the schema filter). With no settings at all, the first tab shows
+ // and says so, so a new plugin still has a settings screen.
+ $settings_tabs = self::settings_tabs();
+ $has_settings = false;
+ foreach (array_keys($settings_tabs) as $slug) {
+ if (WPStarter_Settings::fields_for_tab($slug)) {
+ $has_settings = true;
+ break;
+ }
+ }
+ foreach ($settings_tabs as $slug => $tab) {
+ if ($has_settings && !WPStarter_Settings::fields_for_tab($slug)) {
+ continue;
+ }
+ if (!$has_settings && $tabs) {
+ break;
+ }
+ $tabs[$slug] = array(
+ 'label' => $tab['label'],
+ 'group' => 'settings',
+ 'render' => function () use ($slug, $tab) {
+ WPStarter_Settings_Manager::render_tab($slug, $tab['label'], $tab['description']);
+ },
+ );
+ }
+
+ $tabs += array(
+ 'readme' => array(
+ 'label' => __('Read Me', 'wp-plugin-starter-template'),
+ 'group' => 'about',
+ 'render' => array('WPStarter_Readme_Manager', 'display_tab_content'),
+ ),
+ );
+
+ /**
+ * Filter the admin tabs.
+ *
+ * @param array $tabs Tabs keyed by slug: label, group (settings|discover|about),
+ * render callback, optional capability.
+ */
+ $tabs = (array) apply_filters('wpstarter_admin_tabs', $tabs);
+
+ // Hide tabs the current user cannot use.
+ return array_filter($tabs, function ($tab) {
+ return empty($tab['capability']) || current_user_can($tab['capability']);
+ });
+ }
+
+ /**
+ * Active tab slug, falling back to the first registered tab.
+ *
+ * @return string
+ */
+ public static function get_active_tab() {
+ // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only navigation.
+ $tab = isset($_GET['tab']) ? sanitize_key(wp_unslash($_GET['tab'])) : '';
+ if (self::SEARCH === $tab && '' !== self::search_query()) {
+ return $tab;
+ }
+
+ $tabs = self::get_tabs();
+ $tab = WPStarter_Settings::resolve_tab($tab);
+ if (!isset($tabs[$tab])) {
+ $tab = (string) key($tabs);
+ }
+ return $tab;
+ }
+
+ /**
+ * Current settings search text.
+ *
+ * @return string
+ */
+ public static function search_query() {
+ // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only search.
+ $query = isset($_GET['s']) ? sanitize_text_field(wp_unslash($_GET['s'])) : '';
+ return trim(function_exists('mb_substr') ? mb_substr($query, 0, 100) : substr($query, 0, 100));
+ }
+
+ /**
+ * URL of a tab.
+ *
+ * @param string $tab Tab slug.
+ * @param array $args Extra query args.
+ * @return string
+ */
+ public static function tab_url($tab, array $args = array()) {
+ return add_query_arg(array_merge(array('page' => self::PAGE, 'tab' => $tab), $args), admin_url('options-general.php'));
+ }
+
+ /**
+ * Add the Settings link on the Plugins screen.
+ *
+ * @param array $links Action links.
+ * @return array
+ */
+ public static function plugin_action_links($links) {
+ array_unshift($links, sprintf('%s', esc_url(add_query_arg('page', self::PAGE, admin_url('options-general.php'))), esc_html__('Settings', 'wp-plugin-starter-template')));
+ return $links;
+ }
+
+ /**
+ * Register Settings → WP Plugin Starter.
+ */
+ public static function register_admin_menu() {
+ add_options_page(
+ __('WP Plugin Starter', 'wp-plugin-starter-template'),
+ __('WP Plugin Starter', 'wp-plugin-starter-template'),
+ 'manage_options',
+ self::PAGE,
+ array(__CLASS__, 'render_settings_page')
+ );
+ }
+
+ /**
+ * Enqueue the admin stylesheet and script on our screen only.
+ *
+ * @param string $hook Current admin page hook.
+ */
+ public static function enqueue_assets($hook) {
+ if (self::HOOK !== $hook) {
+ return;
+ }
+
+ $tab = self::get_active_tab();
+ $css = file_exists(WPSTARTER_DIR . 'admin/css/wpstarter-admin.css') ? filemtime(WPSTARTER_DIR . 'admin/css/wpstarter-admin.css') : WPSTARTER_VERSION;
+ $js = file_exists(WPSTARTER_DIR . 'admin/js/wpstarter-admin.js') ? filemtime(WPSTARTER_DIR . 'admin/js/wpstarter-admin.js') : WPSTARTER_VERSION;
+
+ wp_enqueue_style('wpstarter-admin', WPSTARTER_URL . 'admin/css/wpstarter-admin.css', array('dashicons'), $css);
+
+ /**
+ * Filter the admin script's dependencies; enqueue what a tab needs.
+ *
+ * @param string[] $deps Script handles.
+ * @param string $tab Active tab.
+ */
+ $deps = (array) apply_filters('wpstarter_admin_script_deps', array('jquery', 'wp-a11y', 'wp-i18n'), $tab);
+
+ if (self::shows_media_field($tab)) {
+ wp_enqueue_media();
+ }
+
+ wp_enqueue_script('wpstarter-admin', WPSTARTER_URL . 'admin/js/wpstarter-admin.js', $deps, $js, true);
+ wp_set_script_translations('wpstarter-admin', 'wp-plugin-starter-template');
+
+ /**
+ * Filter the data the admin script reads (wpstarterAdmin).
+ *
+ * @param array $data Script data.
+ * @param string $tab Active tab.
+ */
+ wp_localize_script('wpstarter-admin', 'wpstarterAdmin', (array) apply_filters('wpstarter_admin_script_data', array(
+ 'ajaxUrl' => admin_url('admin-ajax.php'),
+ 'nonce' => wp_create_nonce(WPStarter_Settings::NONCE),
+ 'tab' => $tab,
+ 'i18n' => array(
+ 'saving' => __('Saving…', 'wp-plugin-starter-template'),
+ 'saved' => __('Saved', 'wp-plugin-starter-template'),
+ 'saveFailed' => __('Could not save. Please try again.', 'wp-plugin-starter-template'),
+ 'chooseImage' => __('Choose a picture', 'wp-plugin-starter-template'),
+ 'useImage' => __('Use this picture', 'wp-plugin-starter-template'),
+ ),
+ ), $tab));
+
+ /**
+ * Fires after the admin stylesheet and script are enqueued: enqueue
+ * the plugin's own, depending on 'wpstarter-admin'.
+ *
+ * @param string $tab Active tab.
+ */
+ do_action('wpstarter_admin_enqueue', $tab);
+ }
+
+ /**
+ * Whether a tab (or the search results) may show a media field, which
+ * needs the media dialog.
+ *
+ * @param string $tab Tab slug.
+ * @return bool
+ */
+ private static function shows_media_field($tab) {
+ $schema = WPStarter_Settings::schema();
+ foreach ($schema as $field) {
+ if (!isset($field['type']) || 'media' !== $field['type']) {
+ continue;
+ }
+ if (self::SEARCH === $tab) {
+ return true;
+ }
+ $field_tab = isset($field['tab']) ? $field['tab'] : '';
+ if (!$field_tab && isset($field['parent'], $schema[$field['parent']]['tab'])) {
+ $field_tab = $schema[$field['parent']]['tab'];
+ }
+ if ($field_tab === $tab) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Render the page chrome and the active tab.
+ */
+ public static function render_settings_page() {
+ if (!current_user_can('manage_options')) {
+ return;
+ }
+
+ $tabs = self::get_tabs();
+ $active = self::get_active_tab();
+ $links = WPStarter_Setup::header_links();
+ $groups = array(
+ 'settings' => __('Settings', 'wp-plugin-starter-template'),
+ 'discover' => __('Discover', 'wp-plugin-starter-template'),
+ 'about' => __('About', 'wp-plugin-starter-template'),
+ );
+ ?>
+