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).
This commit is contained in:
commit
2f91c3e6af
62 files changed
+10929
No files matched your search
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
/**
|
||||
* Read Me content for the WP Plugin Starter admin tab (from README.md).
|
||||
*
|
||||
* @package WPStarter
|
||||
*/
|
||||
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* README.md contents, with a short fallback if the file is missing.
|
||||
*
|
||||
* @return array{title:string,content:string}
|
||||
*/
|
||||
function wpstarter_get_readme_content() {
|
||||
$readme_path = WPSTARTER_DIR . 'README.md';
|
||||
|
||||
if (is_readable($readme_path)) {
|
||||
$content = (string) file_get_contents($readme_path); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- local file.
|
||||
} else {
|
||||
$content = "# WP Plugin Starter\n\nVersion: " . WPSTARTER_VERSION;
|
||||
}
|
||||
|
||||
return array(
|
||||
'title' => __('Read Me', 'wp-plugin-starter-template'),
|
||||
'content' => $content,
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,434 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!--
|
||||
WP Plugin Starter banner (WordPress.org banners and the Read Me tab).
|
||||
Source: .wordpress-org/banner.svg. Edit that, then run scripts/build-banner.sh.
|
||||
Branding follows wpallstars.com: navy, red and cream, Zilla Slab italic
|
||||
headings, the wpallstars plugin stack in red only, the logo's arc of five stars and its red double stripes (here with cream between).
|
||||
-->
|
||||
|
||||
<svg
|
||||
width="1544"
|
||||
height="500"
|
||||
viewBox="0 0 1544 500"
|
||||
version="1.1"
|
||||
id="svg51"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<title
|
||||
id="title1">WPALLSTARS WP Plugin Starter: Built with AI</title>
|
||||
<defs
|
||||
id="defs21">
|
||||
<linearGradient
|
||||
id="bg"
|
||||
x1="0"
|
||||
y1="0"
|
||||
x2="1"
|
||||
y2="1">
|
||||
<stop
|
||||
offset="0"
|
||||
stop-color="#000c1f"
|
||||
id="stop1" />
|
||||
<stop
|
||||
offset="0.55"
|
||||
stop-color="#001537"
|
||||
id="stop2" />
|
||||
<stop
|
||||
offset="1"
|
||||
stop-color="#0d264d"
|
||||
id="stop3" />
|
||||
</linearGradient>
|
||||
<radialGradient
|
||||
id="glow-blue"
|
||||
cx="1214"
|
||||
cy="270"
|
||||
r="440"
|
||||
gradientUnits="userSpaceOnUse">
|
||||
<stop
|
||||
offset="0"
|
||||
stop-color="#bb0000"
|
||||
stop-opacity="0.45"
|
||||
id="stop4" />
|
||||
<stop
|
||||
offset="1"
|
||||
stop-color="#bb0000"
|
||||
stop-opacity="0"
|
||||
id="stop5" />
|
||||
</radialGradient>
|
||||
<radialGradient
|
||||
id="glow-red"
|
||||
cx="200"
|
||||
cy="580"
|
||||
r="520"
|
||||
gradientUnits="userSpaceOnUse">
|
||||
<stop
|
||||
offset="0"
|
||||
stop-color="#bb0000"
|
||||
stop-opacity="0.2"
|
||||
id="stop6" />
|
||||
<stop
|
||||
offset="1"
|
||||
stop-color="#bb0000"
|
||||
stop-opacity="0"
|
||||
id="stop7" />
|
||||
</radialGradient>
|
||||
<radialGradient
|
||||
id="glow-gold"
|
||||
cx="1214"
|
||||
cy="76"
|
||||
r="120"
|
||||
gradientUnits="userSpaceOnUse">
|
||||
<stop
|
||||
offset="0"
|
||||
stop-color="#ffc94d"
|
||||
stop-opacity="0.45"
|
||||
id="stop8" />
|
||||
<stop
|
||||
offset="1"
|
||||
stop-color="#ffc94d"
|
||||
stop-opacity="0"
|
||||
id="stop9" />
|
||||
</radialGradient>
|
||||
<radialGradient
|
||||
id="dots-fade"
|
||||
cx="1214"
|
||||
cy="260"
|
||||
r="560"
|
||||
gradientUnits="userSpaceOnUse">
|
||||
<stop
|
||||
offset="0"
|
||||
stop-color="#fff"
|
||||
stop-opacity="1"
|
||||
id="stop10" />
|
||||
<stop
|
||||
offset="1"
|
||||
stop-color="#fff"
|
||||
stop-opacity="0"
|
||||
id="stop11" />
|
||||
</radialGradient>
|
||||
<pattern
|
||||
id="dots"
|
||||
width="28"
|
||||
height="28"
|
||||
patternUnits="userSpaceOnUse">
|
||||
<circle
|
||||
cx="14"
|
||||
cy="14"
|
||||
r="1.5"
|
||||
fill="#ffffea"
|
||||
fill-opacity="0.12"
|
||||
id="circle11" />
|
||||
</pattern>
|
||||
<mask
|
||||
id="dots-mask">
|
||||
<rect
|
||||
width="1544"
|
||||
height="500"
|
||||
fill="url(#dots-fade)"
|
||||
id="rect11" />
|
||||
</mask>
|
||||
<linearGradient
|
||||
id="gold"
|
||||
x1="0"
|
||||
y1="0"
|
||||
x2="0"
|
||||
y2="1">
|
||||
<stop
|
||||
offset="0"
|
||||
stop-color="#ffe08a"
|
||||
id="stop12" />
|
||||
<stop
|
||||
offset="1"
|
||||
stop-color="#f5a524"
|
||||
id="stop13" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="gold-text"
|
||||
x1="96"
|
||||
y1="280"
|
||||
x2="700"
|
||||
y2="340"
|
||||
gradientUnits="userSpaceOnUse">
|
||||
<stop
|
||||
offset="0"
|
||||
stop-color="#ffd970"
|
||||
id="stop14" />
|
||||
<stop
|
||||
offset="1"
|
||||
stop-color="#f5a524"
|
||||
id="stop15" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="top-red"
|
||||
x1="0"
|
||||
y1="0"
|
||||
x2="1"
|
||||
y2="1">
|
||||
<stop
|
||||
offset="0"
|
||||
stop-color="#ef4444"
|
||||
id="stop16" />
|
||||
<stop
|
||||
offset="1"
|
||||
stop-color="#bb0000"
|
||||
id="stop17" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="top-cream"
|
||||
x1="0"
|
||||
y1="0"
|
||||
x2="1"
|
||||
y2="1">
|
||||
<stop
|
||||
offset="0"
|
||||
stop-color="#ff8a8a"
|
||||
id="stop18" />
|
||||
<stop
|
||||
offset="1"
|
||||
stop-color="#e03131"
|
||||
id="stop19" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="top-blue"
|
||||
x1="0"
|
||||
y1="0"
|
||||
x2="1"
|
||||
y2="1">
|
||||
<stop
|
||||
offset="0"
|
||||
stop-color="#c81e1e"
|
||||
id="stop20" />
|
||||
<stop
|
||||
offset="1"
|
||||
stop-color="#7a0000"
|
||||
id="stop21" />
|
||||
</linearGradient>
|
||||
<!-- A five-pointed star, radius 1, centred on 0,0 -->
|
||||
<polygon
|
||||
id="star"
|
||||
points="0,-1 0.2245,-0.309 0.9511,-0.309 0.3633,0.118 0.5878,0.809 0,0.382 -0.5878,0.809 -0.3633,0.118 -0.9511,-0.309 -0.2245,-0.309" />
|
||||
</defs>
|
||||
<!-- Background -->
|
||||
<rect
|
||||
width="1544"
|
||||
height="500"
|
||||
fill="url(#bg)"
|
||||
id="rect21" />
|
||||
<rect
|
||||
width="1544"
|
||||
height="500"
|
||||
fill="url(#glow-red)"
|
||||
id="rect22" />
|
||||
<rect
|
||||
width="1544"
|
||||
height="500"
|
||||
fill="url(#glow-blue)"
|
||||
id="rect23" />
|
||||
<rect
|
||||
width="1544"
|
||||
height="500"
|
||||
fill="url(#dots)"
|
||||
mask="url(#dots-mask)"
|
||||
id="rect24" />
|
||||
<!-- wpallstars.com's red double lines, with a cream line between them. -->
|
||||
<g
|
||||
id="g30">
|
||||
<rect
|
||||
y="0"
|
||||
width="1544"
|
||||
height="6"
|
||||
fill="#bb0000"
|
||||
id="rect25" />
|
||||
<rect
|
||||
y="6"
|
||||
width="1544"
|
||||
height="5"
|
||||
fill="#ffffea"
|
||||
id="rect26" />
|
||||
<rect
|
||||
y="11"
|
||||
width="1544"
|
||||
height="6"
|
||||
fill="#bb0000"
|
||||
id="rect27" />
|
||||
<rect
|
||||
y="483"
|
||||
width="1544"
|
||||
height="6"
|
||||
fill="#bb0000"
|
||||
id="rect28" />
|
||||
<rect
|
||||
y="489"
|
||||
width="1544"
|
||||
height="5"
|
||||
fill="#ffffea"
|
||||
id="rect29" />
|
||||
<rect
|
||||
y="494"
|
||||
width="1544"
|
||||
height="6"
|
||||
fill="#bb0000"
|
||||
id="rect30" />
|
||||
</g>
|
||||
<!-- The picture: scaled down a little so it keeps clear of the stripes -->
|
||||
<g
|
||||
transform="translate(1214 248) scale(0.86) translate(-1214 -248)"
|
||||
id="g48">
|
||||
<!-- The stack: red, cream and blue layers, each with a top face and two sides -->
|
||||
<g
|
||||
stroke-linejoin="round"
|
||||
id="g43">
|
||||
<g
|
||||
transform="translate(0 350)"
|
||||
id="g33">
|
||||
<polygon
|
||||
points="1064,0 1214,87 1214,117 1064,30"
|
||||
fill="#5c0000"
|
||||
id="polygon30" />
|
||||
<polygon
|
||||
points="1214,87 1364,0 1364,30 1214,117"
|
||||
fill="#420000"
|
||||
id="polygon31" />
|
||||
<polygon
|
||||
points="1214,-87 1364,0 1214,87 1064,0"
|
||||
fill="url(#top-blue)"
|
||||
id="polygon32" />
|
||||
<polygon
|
||||
points="1214,-61 1319,0 1214,61 1109,0"
|
||||
fill="none"
|
||||
stroke="#ffffff"
|
||||
stroke-opacity="0.3"
|
||||
stroke-width="2"
|
||||
id="polygon33" />
|
||||
</g>
|
||||
<g
|
||||
transform="translate(0 294)"
|
||||
id="g37">
|
||||
<polygon
|
||||
points="1064,0 1214,87 1214,117 1064,30"
|
||||
fill="#c22424"
|
||||
id="polygon34" />
|
||||
<polygon
|
||||
points="1214,87 1364,0 1364,30 1214,117"
|
||||
fill="#9e1414"
|
||||
id="polygon35" />
|
||||
<polygon
|
||||
points="1214,-87 1364,0 1214,87 1064,0"
|
||||
fill="url(#top-cream)"
|
||||
id="polygon36" />
|
||||
<polygon
|
||||
points="1214,-61 1319,0 1214,61 1109,0"
|
||||
fill="none"
|
||||
stroke="#001537"
|
||||
stroke-opacity="0.18"
|
||||
stroke-width="2"
|
||||
id="polygon37" />
|
||||
</g>
|
||||
<g
|
||||
transform="translate(0 238)"
|
||||
id="g42">
|
||||
<polygon
|
||||
points="1064,0 1214,87 1214,117 1064,30"
|
||||
fill="#940000"
|
||||
id="polygon38" />
|
||||
<polygon
|
||||
points="1214,87 1364,0 1364,30 1214,117"
|
||||
fill="#700000"
|
||||
id="polygon39" />
|
||||
<polygon
|
||||
points="1214,-87 1364,0 1214,87 1064,0"
|
||||
fill="url(#top-red)"
|
||||
id="polygon40" />
|
||||
<!-- The WordPress Plugins icon (Dashicons admin-plugins, GPLv2 or later),
|
||||
laid flat on the top face: icon x runs along the top-right edge and
|
||||
icon y along the top-left edge, each 20-unit side 66% of the face.
|
||||
Turned 45 degrees so the plug points along the back-left edge. -->
|
||||
<g
|
||||
transform="matrix(4.95 2.871 -4.95 2.871 1214 -57.42)"
|
||||
id="g41">
|
||||
<path
|
||||
fill="#5c0000"
|
||||
fill-opacity="0.55"
|
||||
transform="translate(0.3 0.3) rotate(-45 10 10)"
|
||||
d="M13.11 4.36L9.87 7.6 8 5.73l3.24-3.24c.35-.34 1.05-.2 1.56.32.52.51.66 1.21.31 1.55zm-8 1.77l.91-1.12 9.01 9.01-1.19.84c-.71.71-2.63 1.16-3.82 1.16H6.14L4.9 17.26c-.59.59-1.54.59-2.12 0-.59-.58-.59-1.53 0-2.12l1.24-1.24v-3.88c0-1.13.4-3.19 1.09-3.89zm7.26 3.97l3.24-3.24c.34-.35 1.04-.21 1.55.31.52.51.66 1.21.31 1.55l-3.24 3.25z"
|
||||
id="path40" />
|
||||
<path
|
||||
fill="#ffffea"
|
||||
transform="rotate(-45 10 10)"
|
||||
d="M13.11 4.36L9.87 7.6 8 5.73l3.24-3.24c.35-.34 1.05-.2 1.56.32.52.51.66 1.21.31 1.55zm-8 1.77l.91-1.12 9.01 9.01-1.19.84c-.71.71-2.63 1.16-3.82 1.16H6.14L4.9 17.26c-.59.59-1.54.59-2.12 0-.59-.58-.59-1.53 0-2.12l1.24-1.24v-3.88c0-1.13.4-3.19 1.09-3.89zm7.26 3.97l3.24-3.24c.34-.35 1.04-.21 1.55.31.52.51.66 1.21.31 1.55l-3.24 3.25z"
|
||||
id="path41" />
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<!-- The logo's arc of five stars: gold in the middle, floating above the stack -->
|
||||
<circle
|
||||
cx="1214"
|
||||
cy="76"
|
||||
r="120"
|
||||
fill="url(#glow-gold)"
|
||||
id="circle43" />
|
||||
<g
|
||||
stroke-linejoin="round"
|
||||
id="g47">
|
||||
<g
|
||||
fill="#bb0000"
|
||||
stroke="#ffffea"
|
||||
stroke-width="5"
|
||||
id="g46">
|
||||
<use
|
||||
xlink:href="#star"
|
||||
transform="translate(1043 150) scale(17)"
|
||||
stroke-width="0.3"
|
||||
id="use43" />
|
||||
<use
|
||||
xlink:href="#star"
|
||||
transform="translate(1110 100) scale(26)"
|
||||
stroke-width="0.2"
|
||||
id="use44" />
|
||||
<use
|
||||
xlink:href="#star"
|
||||
transform="translate(1318 100) scale(26)"
|
||||
stroke-width="0.2"
|
||||
id="use45" />
|
||||
<use
|
||||
xlink:href="#star"
|
||||
transform="translate(1385 150) scale(17)"
|
||||
stroke-width="0.3"
|
||||
id="use46" />
|
||||
</g>
|
||||
<use
|
||||
xlink:href="#star"
|
||||
transform="translate(1214 78) scale(54)"
|
||||
fill="url(#gold)"
|
||||
stroke="#ffffea"
|
||||
stroke-width="0.11"
|
||||
id="use47" />
|
||||
</g>
|
||||
</g>
|
||||
<!-- Words -->
|
||||
<g
|
||||
font-family="Zilla Slab"
|
||||
text-rendering="geometricPrecision"
|
||||
id="g51">
|
||||
<path
|
||||
d="m 123.893,113.015 h -1.431 L 117.683,128 h -2.214 l -4.293,-12.366 h -0.135 L 106.883,128 h -2.241 l -5.076,-14.985 h -1.512 v -2.565 h 6.669 v 2.565 h -1.944 l 3.024,10.044 h 0.135 l 3.483,-10.044 h -1.539 v -2.565 h 6.642 v 2.565 h -1.431 l 3.483,10.179 h 0.135 l 2.754,-10.179 h -2.16 v -2.565 h 6.588 z m 20.60601,3.024 q 0,2.889 -1.863,4.374 -1.836,1.458 -4.428,1.458 h -2.97 v 3.564 h 2.808 V 128 h -7.83 v -2.565 h 2.025 v -12.42 h -2.025 v -2.565 h 7.83 q 3.267,0 4.86,1.566 1.593,1.566 1.593,4.023 z m -3.213,0.054 q 0,-1.458 -0.837,-2.268 -0.81,-0.81 -2.754,-0.81 h -2.457 v 6.318 h 2.457 q 1.917,0 2.754,-0.864 0.837,-0.891 0.837,-2.376 z M 166.72505,128 h -6.723 v -2.565 h 1.782 l -0.891,-2.403 h -6.561 l -0.945,2.403 h 1.971 V 128 h -6.642 v -2.565 h 1.62 l 5.292,-12.42 h -2.565 v -2.565 h 5.643 l 6.318,14.985 h 1.701 z m -6.642,-7.155 -2.295,-6.102 h -0.162 l -2.43,6.102 z M 185.71107,128 h -12.987 v -2.565 h 2.025 v -12.42 h -2.025 v -2.565 h 7.641 v 2.565 h -2.619 v 12.42 h 5.4 l 0.54,-3.645 2.565,0.216 z m 19.93099,0 h -12.987 v -2.565 h 2.025 v -12.42 h -2.025 v -2.565 h 7.641 v 2.565 h -2.619 v 12.42 h 5.4 l 0.54,-3.645 2.565,0.216 z m 19.68799,-5.157 q 0,2.619 -1.836,4.023 -1.809,1.404 -4.806,1.404 -1.782,0 -3.456,-0.432 -1.647,-0.432 -2.619,-1.242 l 0.486,-4.023 2.403,0.243 -0.054,2.16 q 0.891,0.459 1.62,0.594 0.756,0.108 1.404,0.108 1.566,0 2.592,-0.486 1.053,-0.513 1.053,-2.025 0,-1.647 -1.188,-2.187 -1.188,-0.567 -2.727,-0.864 -1.242,-0.243 -2.511,-0.648 -1.269,-0.405 -2.106,-1.35 -0.837,-0.945 -0.837,-2.781 0,-2.754 1.863,-3.942 1.89,-1.215 4.212,-1.215 1.62,0 3.132,0.486 1.539,0.486 2.403,1.269 l 0.162,3.699 -2.403,0.216 -0.243,-2.268 q -0.756,-0.432 -1.512,-0.594 -0.756,-0.162 -1.458,-0.162 -1.269,0 -2.106,0.513 -0.837,0.513 -0.837,1.647 0,1.323 1.026,1.809 1.053,0.459 2.349,0.756 1.107,0.243 2.511,0.648 1.404,0.405 2.43,1.458 1.053,1.026 1.053,3.186 z m 22.65801,-6.669 -2.592,0.216 -0.54,-3.375 h -3.429 v 12.42 h 2.943 V 128 h -8.775 v -2.565 h 2.835 v -12.42 h -3.456 l -0.54,3.375 -2.565,-0.216 0.54,-5.724 h 15.039 z M 270.18705,128 h -6.723 v -2.565 h 1.782 l -0.891,-2.403 h -6.561 l -0.945,2.403 h 1.971 V 128 h -6.642 v -2.565 h 1.62 l 5.292,-12.42 h -2.565 v -2.565 h 5.643 l 6.318,14.985 h 1.701 z m -6.642,-7.155 -2.295,-6.102 h -0.162 l -2.43,6.102 z M 291.81908,128 h -4.104 l -3.618,-6.939 h -2.889 v 4.374 h 2.052 V 128 h -7.074 v -2.565 h 2.025 v -12.42 h -2.025 v -2.565 h 7.803 q 3.267,0 4.887,1.377 1.62,1.35 1.62,3.807 0,1.755 -0.945,3.024 -0.918,1.242 -2.565,1.782 l 2.673,4.995 h 2.16 z m -4.509,-12.285 q 0,-1.485 -0.864,-2.079 -0.864,-0.621 -2.943,-0.621 h -2.295 v 5.643 h 2.268 q 2.16,0 2.997,-0.756 0.837,-0.783 0.837,-2.187 z m 23.62998,7.128 q 0,2.619 -1.836,4.023 -1.809,1.404 -4.806,1.404 -1.782,0 -3.456,-0.432 -1.647,-0.432 -2.619,-1.242 l 0.486,-4.023 2.403,0.243 -0.054,2.16 q 0.891,0.459 1.62,0.594 0.756,0.108 1.404,0.108 1.566,0 2.592,-0.486 1.053,-0.513 1.053,-2.025 0,-1.647 -1.188,-2.187 -1.188,-0.567 -2.727,-0.864 -1.242,-0.243 -2.511,-0.648 -1.269,-0.405 -2.106,-1.35 -0.837,-0.945 -0.837,-2.781 0,-2.754 1.863,-3.942 1.89,-1.215 4.212,-1.215 1.62,0 3.132,0.486 1.539,0.486 2.403,1.269 l 0.162,3.699 -2.403,0.216 -0.243,-2.268 q -0.756,-0.432 -1.512,-0.594 -0.756,-0.162 -1.458,-0.162 -1.269,0 -2.106,0.513 -0.837,0.513 -0.837,1.647 0,1.323 1.026,1.809 1.053,0.459 2.349,0.756 1.107,0.243 2.511,0.648 1.404,0.405 2.43,1.458 1.053,1.026 1.053,3.186 z"
|
||||
id="text48"
|
||||
style="font-weight:600;font-size:27px;letter-spacing:5;fill:#ff999b"
|
||||
aria-label="WPALLSTARS" />
|
||||
<path
|
||||
d="M 192.464,184.256 H 187.16 L 163.552,240 h -9.464 l -10.712,-43.68 h -0.52 L 123.72,240 h -9.672 L 100.944,184.256 H 95.328 L 96.576,172.4 h 25.792 l -1.248,11.856 h -5.928 l 6.24,32.656 h 0.624 l 14.976,-32.656 h -5.096 l 1.248,-11.856 h 25.896 l -1.248,11.856 h -4.68 l 8.008,33.28 h 0.624 l 11.96,-33.28 h -6.864 l 1.248,-11.856 h 25.584 z m -0.37598,43.888 h 6.968 l 4.576,-43.888 h -6.968 l 1.248,-11.856 h 29.744 q 11.128,0 16.848,5.616 5.72,5.512 5.72,14.976 0,12.272 -8.216,18.304 -8.112,6.032 -18.408,6.032 h -9.984 l -1.144,10.816 h 9.984 l -1.248,11.856 h -30.368 z m 30.264,-22.568 q 7.696,0 10.504,-3.432 2.912,-3.536 2.912,-8.528 0,-4.576 -2.392,-6.968 -2.392,-2.392 -8.632,-2.392 h -7.696 l -2.184,21.32 z m 46.464,22.568 h 6.968 l 4.576,-43.888 h -6.968 l 1.248,-11.856 h 29.744 q 11.128,0 16.848,5.616 5.72,5.512 5.72,14.976 0,12.272 -8.216,18.304 -8.112,6.032 -18.408,6.032 h -9.984 l -1.144,10.816 h 9.984 l -1.248,11.856 h -30.368 z m 30.264,-22.568 q 7.696,0 10.504,-3.432 2.912,-3.536 2.912,-8.528 0,-4.576 -2.392,-6.968 -2.392,-2.392 -8.632,-2.392 h -7.696 l -2.184,21.32 z m 31.55191,-34.32 3.224,-2.808 h 16.432 l -7.488,61.36 h 6.032 l -0.936,7.592 -3.12,2.6 h -16.12 l 7.488,-61.36 h -6.448 z m 73.98399,58.552 -0.936,7.384 -3.224,2.808 h -14.456 l -0.52,-9.048 q -2.392,4.472 -6.136,7.28 -3.744,2.704 -8.944,2.704 -6.344,0 -10.608,-3.952 -4.16,-3.952 -3.12,-12.272 l 2.6,-21.424 h -6.032 l 0.936,-7.592 3.12,-2.6 h 16.016 l -3.224,26.416 q -0.728,6.136 0.832,8.736 1.664,2.496 5.512,2.496 4.68,0 6.968,-3.12 2.392,-3.12 3.016,-8.008 v 0.104 l 3.224,-26.624 h 12.896 l -4.472,36.712 z m 41.32793,-36.712 h 16.952 l -0.936,7.592 -3.12,2.6 h -3.328 l -4.264,34.32 q -1.248,9.984 -6.864,17.68 -5.616,7.8 -18.616,7.8 -6.656,0 -12.584,-3.328 -5.824,-3.224 -7.592,-11.336 l 9.152,-5.2 q 3.432,8.736 11.856,8.736 9.984,0 11.752,-14.352 l 0.728,-6.448 q -2.392,4.056 -6.24,6.968 -3.848,2.912 -9.36,2.912 -7.696,0 -11.336,-5.824 -3.64,-5.824 -3.64,-13.936 0,-6.552 2.392,-13.208 2.392,-6.76 7.176,-11.336 4.888,-4.576 12.48,-4.576 10.088,0 13.104,8.32 z m -13.312,9.256 q -4.992,0 -7.592,5.2 -2.6,5.2 -2.6,12.272 0,4.888 1.456,8.008 1.456,3.016 4.888,3.016 4.16,0 7.488,-3.952 3.328,-3.952 4.368,-8.424 l 0.624,-4.992 q 0.208,-5.408 -2.496,-8.216 -2.704,-2.912 -6.136,-2.912 z m 39.5601,-17.576 1.456,-13.936 h 12.48 l -1.456,13.936 z m -8.216,11.128 3.224,-2.808 h 16.536 l -4.472,36.712 h 5.928 l -0.936,7.592 -3.12,2.6 h -16.016 l 4.472,-36.712 h -6.552 z m 30.51203,-2.808 h 14.456 l 0.52,8.944 q 2.392,-4.472 6.136,-7.176 3.744,-2.704 8.944,-2.704 6.344,0 10.504,3.952 4.264,3.952 3.224,12.272 l -2.6,21.424 h 6.032 l -0.936,7.592 -3.12,2.6 h -16.016 l 3.224,-26.416 q 0.728,-6.032 -0.936,-8.632 -1.56,-2.6 -5.408,-2.6 -4.368,0 -6.76,2.704 -2.392,2.704 -3.12,7.176 l -3.328,27.768 h -12.896 l 4.472,-36.712 h -6.552 l 0.936,-7.384 z m 118.53613,25.688 q 0,10.816 -7.176,16.536 -7.072,5.72 -19.76,5.72 -6.968,0 -13.52,-1.768 -6.448,-1.768 -9.984,-5.304 l 3.848,-16.744 10.816,1.04 -0.936,8.216 q 3.224,1.872 5.616,2.288 2.392,0.312 3.744,0.312 5.824,0 9.152,-1.768 3.432,-1.872 3.432,-7.488 0,-5.096 -2.912,-6.552 -2.912,-1.56 -8.632,-2.6 -4.576,-0.832 -9.048,-2.496 -4.368,-1.768 -7.28,-5.304 -2.912,-3.64 -2.912,-10.4 0,-7.8 3.848,-12.376 3.952,-4.68 9.88,-6.656 6.032,-2.08 12.272,-2.08 6.864,0 12.48,2.08 5.72,1.976 8.632,5.408 l -0.936,14.664 -11.024,1.04 -0.104,-7.904 q -2.392,-1.768 -4.888,-2.392 -2.496,-0.624 -4.992,-0.624 -4.368,0 -7.488,1.664 -3.12,1.56 -3.12,5.408 0,4.056 3.328,5.616 3.328,1.56 7.696,2.496 4.368,0.936 8.944,2.704 4.68,1.664 7.8,5.616 3.224,3.952 3.224,11.648 z m 5.44796,-22.464 3.536,-3.224 h 3.016 l 1.248,-10.088 4.056,-4.16 h 9.36 l -1.768,14.248 h 14.04 l -0.936,6.864 -3.536,3.328 h -10.816 l -2.6,21.424 q -0.728,6.136 3.12,6.136 1.664,0 3.224,-1.456 1.664,-1.56 3.016,-6.968 l 9.048,3.016 q -1.976,8.112 -5.824,11.856 -3.848,3.744 -10.816,3.744 -7.488,0 -11.648,-3.848 -4.056,-3.952 -3.224,-11.232 l 2.808,-22.672 h -6.24 z m 85.11207,-3.224 q -1.352,4.264 -2.392,8.32 -0.936,4.056 -1.456,8.32 l -2.496,20.072 h 6.032 l -0.936,7.592 -3.12,2.6 h -14.04 l -0.416,-9.568 q -2.184,4.368 -5.928,7.488 -3.744,3.12 -9.464,3.12 -7.696,0 -11.544,-5.824 -3.848,-5.824 -3.848,-14.248 0,-6.344 2.288,-13 2.288,-6.656 6.968,-11.232 4.784,-4.576 12.376,-4.576 10.192,0 13.312,8.736 0.52,-1.976 0.936,-3.848 0.52,-1.976 1.248,-3.952 z m -25.896,9.256 q -4.992,0 -7.384,5.304 -2.392,5.2 -2.392,11.752 0,11.44 6.656,11.44 4.264,0 7.28,-4.056 3.016,-4.056 3.952,-8.632 l 0.416,-3.536 q 0.624,-5.824 -2.184,-9.048 -2.704,-3.224 -6.344,-3.224 z m 35.50396,-9.256 h 13.416 l 0.728,12.896 q 1.56,-4.888 4.264,-9.36 2.808,-4.472 7.072,-4.472 2.912,0 5.096,1.872 l -2.08,13.832 q -2.912,-1.56 -5.408,-1.56 -7.072,0 -8.736,7.28 l -2.288,16.224 h 8.944 l -0.936,7.384 -3.224,2.808 h -25.064 l 0.936,-7.384 3.224,-2.808 h 3.12 l 3.224,-26.52 h -6.448 l 0.936,-7.384 z m 31.13605,3.224 3.536,-3.224 h 3.016 l 1.248,-10.088 4.056,-4.16 h 9.36 l -1.768,14.2Line truncated
|
||||
id="text49"
|
||||
style="font-style:italic;font-weight:700;font-size:104px;letter-spacing:-1;fill:#ffffea"
|
||||
aria-label="WP Plugin Starter" />
|
||||
<path
|
||||
d="m 124.944,279.2 q 5.76,0 9.144,2.736 3.456,2.664 3.456,7.272 0,3.816 -2.448,6.912 -2.376,3.096 -6.768,3.816 v 0.288 q 4.248,0.72 6.408,3.528 2.16,2.808 2.16,7.056 0,5.904 -4.104,10.584 Q 128.688,326 120.336,326 H 96.648 l 0.864,-8.208 h 4.824 l 3.168,-30.384 h -4.824 l 0.864,-8.208 z m -5.112,19.152 q 4.392,0 5.904,-1.944 1.584,-1.944 1.584,-4.248 0,-2.88 -1.656,-3.816 -1.584,-0.936 -4.032,-0.936 h -6.84 l -1.152,10.944 z m -1.08,19.44 q 8.208,0 8.208,-6.624 0,-5.328 -6.336,-5.328 h -7.776 l -1.224,11.952 z m 57.24003,1.152 -0.648,5.112 -2.232,1.944 h -10.008 l -0.36,-6.264 q -1.656,3.096 -4.248,5.04 -2.592,1.872 -6.192,1.872 -4.392,0 -7.344,-2.736 -2.88,-2.736 -2.16,-8.496 l 1.8,-14.832 h -4.176 l 0.648,-5.256 2.16,-1.8 h 11.088 l -2.232,18.288 q -0.504,4.248 0.576,6.048 1.152,1.728 3.816,1.728 3.24,0 4.824,-2.16 1.656,-2.16 2.088,-5.544 v 0.072 l 2.232,-18.432 h 8.928 l -3.096,25.416 z m 10.29594,-31.176 1.008,-9.648 h 8.64 l -1.008,9.648 z m -5.688,7.704 2.232,-1.944 h 11.448 l -3.096,25.416 h 4.104 l -0.648,5.256 -2.16,1.8 h -11.088 l 3.096,-25.416 h -4.536 z m 20.80802,-17.064 2.232,-1.944 h 11.376 l -5.184,42.48 h 4.176 l -0.648,5.256 -2.16,1.8 h -11.16 l 5.184,-42.48 h -4.464 z m 16.84799,17.352 2.448,-2.232 h 2.088 l 0.864,-6.984 2.808,-2.88 h 6.48 l -1.224,9.864 h 9.72 l -0.648,4.752 -2.448,2.304 h -7.488 l -1.8,14.832 q -0.504,4.248 2.16,4.248 1.152,0 2.232,-1.008 1.152,-1.08 2.088,-4.824 l 6.264,2.088 q -1.368,5.616 -4.032,8.208 -2.664,2.592 -7.488,2.592 -5.184,0 -8.064,-2.664 -2.808,-2.736 -2.232,-7.776 l 1.944,-15.696 h -4.32 z m 72.21606,19.584 h 0.36 q 3.312,-4.608 4.68,-7.92 1.44,-3.384 1.44,-6.912 0,-3.456 -1.44,-6.984 h 10.008 q 0.792,4.392 -0.216,9.072 -1.008,4.68 -4.824,9.792 -1.872,2.448 -5.328,6.48 -3.456,4.032 -6.624,7.128 h -3.744 l -4.752,-18.72 h -0.432 l -10.512,18.72 h -4.752 l -6.912,-25.416 h -3.168 l 0.648,-5.112 2.232,-1.944 h 14.616 l -0.648,5.112 -2.232,1.944 h -2.232 l 2.52,12.816 h 0.432 l 10.368,-19.872 h 5.328 z m 24.04794,-27.576 1.008,-9.648 h 8.64 l -1.008,9.648 z m -5.688,7.704 2.232,-1.944 h 11.448 l -3.096,25.416 h 4.104 l -0.648,5.256 -2.16,1.8 h -11.088 l 3.096,-25.416 h -4.536 z m 19.15202,0.288 2.448,-2.232 h 2.088 l 0.864,-6.984 2.808,-2.88 h 6.48 l -1.224,9.864 h 9.72 l -0.648,4.752 -2.448,2.304 h -7.488 l -1.8,14.832 q -0.504,4.248 2.16,4.248 1.152,0 2.232,-1.008 1.152,-1.08 2.088,-4.824 l 6.264,2.088 q -1.368,5.616 -4.032,8.208 -2.664,2.592 -7.488,2.592 -5.184,0 -8.064,-2.664 -2.808,-2.736 -2.232,-7.776 l 1.944,-15.696 h -4.32 z m 27.28803,-17.352 2.232,-1.944 h 11.088 l -2.736,22.536 q 1.584,-2.736 4.104,-4.392 2.52,-1.728 5.904,-1.728 4.392,0 7.272,2.736 2.952,2.736 2.232,8.496 l -1.8,14.832 h 4.176 l -0.648,5.256 -2.16,1.8 h -11.088 l 2.232,-18.288 q 0.576,-4.176 -0.576,-5.976 -1.152,-1.8 -3.816,-1.8 -3.096,0 -4.68,1.944 -1.584,1.944 -2.16,5.112 l -2.304,19.008 h -8.928 l 5.184,-42.48 h -4.176 z m 45.14409,39.384 h 4.104 l 15.984,-30.384 h -6.192 l 0.864,-8.208 h 15.408 l 12.168,38.592 h 4.248 l -0.864,8.208 h -17.856 l 0.864,-8.208 h 3.6 l -1.224,-4.752 h -15.264 l -2.304,4.752 h 4.176 l -0.864,8.208 h -17.712 z m 25.632,-24.84 -6.408,13.176 h 10.152 l -3.312,-13.176 z m 23.75999,24.84 h 4.824 l 3.168,-30.384 h -4.824 l 0.864,-8.208 h 18.936 l -0.864,8.208 h -4.896 l -3.168,30.384 h 4.896 l -0.864,8.208 h -18.936 z"
|
||||
id="text50"
|
||||
style="font-style:italic;font-weight:700;font-size:72px;fill:url(#gold-text)"
|
||||
aria-label="Built with AI" />
|
||||
<path
|
||||
d="m 119.912,396 h -7.887 v -2.607 h 2.541 l -1.32,-3.564 H 104.6 l -1.419,3.564 h 2.805 V 396 h -7.821 v -2.607 h 1.98 l 6.765,-16.236 h -3.3 v -2.607 h 6.468 l 7.722,18.843 h 2.112 z m -7.524,-8.415 -3.267,-8.712 h -0.198 l -3.432,8.712 z m 27.852,3.3 q -0.264,1.947 -1.749,3.696 -1.452,1.749 -4.653,1.749 -3.102,0 -5.016,-1.914 -1.881,-1.947 -1.881,-5.544 0,-3.333 1.881,-5.61 1.881,-2.31 5.247,-2.31 1.386,0 2.673,0.33 1.287,0.33 2.442,1.155 l 0.594,3.828 -2.343,0.264 -0.528,-2.475 q -1.254,-0.726 -2.871,-0.726 -2.079,0 -3.069,1.518 -0.99,1.485 -0.99,3.861 0,2.475 1.056,3.861 1.056,1.386 3.102,1.386 3.069,0 3.663,-3.564 z m 9.24002,5.115 h -7.656 v -2.244 h 2.376 v -18.183 h -2.475 v -2.277 h 5.379 v 20.46 h 2.376 z m 15.27898,-4.356 q -0.231,1.914 -1.881,3.3 -1.65,1.386 -4.488,1.386 -3.201,0 -5.181,-1.914 -1.98,-1.914 -1.98,-5.544 0,-3.432 1.848,-5.676 1.848,-2.244 5.115,-2.244 3.036,0 4.719,1.881 1.683,1.881 1.716,4.554 0,0.891 -0.198,1.881 h -10.164 q 0.198,4.686 4.323,4.686 1.716,0 2.574,-0.825 0.891,-0.825 1.188,-1.98 z m -3.135,-4.323 q 0.132,-1.452 -0.726,-2.772 -0.825,-1.353 -2.772,-1.353 -1.815,0 -2.739,1.221 -0.891,1.188 -1.089,2.904 z m 6.73203,-4.95 q 1.353,-0.792 2.64,-1.089 1.32,-0.33 2.871,-0.33 2.508,0 3.96,1.221 1.485,1.221 1.485,3.762 v 6.831 q 0,1.452 1.155,1.452 0.462,0 0.891,-0.165 l 0.033,1.716 q -0.924,0.561 -2.145,0.561 -2.673,0 -2.838,-2.97 v -0.033 q -0.66,1.155 -1.914,2.079 -1.221,0.924 -3.003,0.924 -1.518,0 -3.036,-0.924 -1.485,-0.924 -1.485,-3.234 0,-2.772 2.178,-3.63 2.211,-0.891 4.719,-0.891 1.518,0 2.541,0.132 v -1.089 q 0,-1.452 -0.561,-2.475 -0.561,-1.023 -2.508,-1.023 -1.584,0 -2.673,0.561 l -0.561,2.409 -2.343,-0.264 z m 8.052,7.953 v -0.858 q -0.594,-0.066 -1.254,-0.099 -0.627,-0.066 -1.287,-0.066 -1.518,0 -2.706,0.495 -1.188,0.495 -1.188,2.046 0,1.188 0.726,1.716 0.759,0.528 1.716,0.528 1.419,0 2.64,-0.99 1.221,-0.99 1.353,-2.772 z M 200.72902,396 h -7.656 v -2.244 h 2.376 v -6.369 q 0,-2.244 -0.858,-3.168 -0.858,-0.957 -2.277,-0.957 -1.848,0 -2.937,1.221 -1.089,1.221 -1.155,2.838 v 6.435 h 2.376 V 396 h -7.656 v -2.244 h 2.376 v -10.23 h -2.475 v -2.277 h 5.379 v 2.508 q 1.551,-2.805 5.016,-2.805 2.145,0 3.63,1.32 1.485,1.32 1.485,4.125 v 7.359 h 2.376 z m 20.95501,-4.323 q 0,2.211 -1.848,3.432 -1.815,1.221 -4.62,1.221 -3.399,0 -5.775,-1.452 l 0.462,-3.564 2.277,0.231 -0.066,1.914 q 0.66,0.33 1.419,0.462 0.792,0.132 1.551,0.132 1.419,0 2.541,-0.528 1.155,-0.528 1.155,-1.65 0,-1.089 -0.924,-1.518 -0.924,-0.462 -2.31,-0.693 -1.353,-0.231 -2.706,-0.561 -1.353,-0.363 -2.277,-1.188 -0.924,-0.858 -0.924,-2.574 0,-2.475 1.815,-3.432 1.848,-0.957 3.927,-0.957 1.518,0 2.838,0.396 1.353,0.363 2.442,0.957 l 0.33,3.564 -2.31,0.264 -0.33,-2.31 q -1.353,-0.627 -2.838,-0.627 -1.221,0 -2.013,0.462 -0.792,0.462 -0.792,1.518 0,1.023 0.891,1.452 0.924,0.429 2.244,0.66 1.353,0.198 2.673,0.561 1.353,0.363 2.244,1.221 0.924,0.858 0.924,2.607 z m 13.62901,0.165 q -0.627,4.488 -4.95,4.488 -2.277,0 -3.564,-1.221 -1.254,-1.254 -1.254,-3.465 v -8.184 h -2.277 v -2.211 h 2.277 v -4.653 h 2.871 v 4.653 h 5.643 v 2.211 h -5.643 v 7.755 q 0,2.64 2.277,2.64 0.726,0 1.452,-0.462 0.726,-0.495 1.023,-2.244 z m 2.60698,-9.471 q 1.353,-0.792 2.64,-1.089 1.32,-0.33 2.871,-0.33 2.508,0 3.96,1.221 1.485,1.221 1.485,3.762 v 6.831 q 0,1.452 1.155,1.452 0.462,0 0.891,-0.165 l 0.033,1.716 q -0.924,0.561 -2.145,0.561 -2.673,0 -2.838,-2.97 v -0.033 q -0.66,1.155 -1.914,2.079 -1.221,0.924 -3.003,0.924 -1.518,0 -3.036,-0.924 -1.485,-0.924 -1.485,-3.234 0,-2.772 2.178,-3.63 2.211,-0.891 4.719,-0.891 1.518,0 2.541,0.132 v -1.089 q 0,-1.452 -0.561,-2.475 -0.561,-1.023 -2.508,-1.023 -1.584,0 -2.673,0.561 l -0.561,2.409 -2.343,-0.264 z m 8.052,7.953 v -0.858 q -0.594,-0.066 -1.254,-0.099 -0.627,-0.066 -1.287,-0.066 -1.518,0 -2.706,0.495 -1.188,0.495 -1.188,2.046 0,1.188 0.726,1.716 0.759,0.528 1.716,0.528 1.419,0 2.64,-0.99 1.221,-0.99 1.353,-2.772 z m 15.213,-9.306 q 1.551,0 2.64,0.792 l 0.594,4.125 -2.277,0.264 -0.561,-2.541 q -0.561,-0.231 -1.155,-0.231 -1.122,0 -1.881,0.957 -0.759,0.924 -0.759,2.442 v 6.93 h 3.168 V 396 h -8.448 v -2.244 h 2.376 v -10.23 h -2.475 v -2.277 h 5.181 v 2.706 q 0.429,-1.254 1.32,-2.079 0.924,-0.858 2.277,-0.858 z m 16.86302,10.824 q -0.627,4.488 -4.95,4.488 -2.277,0 -3.564,-1.221 -1.254,-1.254 -1.254,-3.465 v -8.184 h -2.277 v -2.211 h 2.277 v -4.653 h 2.871 v 4.653 h 5.643 v 2.211 h -5.643 v 7.755 q 0,2.64 2.277,2.64 0.726,0 1.452,-0.462 0.726,-0.495 1.023,-2.244 z m 15.80697,-18.876 q 1.023,0 1.947,0.231 0.957,0.231 1.782,0.891 l 0.594,3.597 -2.31,0.297 -0.627,-2.475 q -0.693,-0.231 -1.551,-0.231 -2.343,0 -2.343,3.036 v 2.937 h 3.795 v 2.211 h -3.795 v 10.296 h 3.102 V 396 h -8.382 v -2.244 h 2.409 V 383.46 h -2.046 v -2.211 h 2.046 v -2.607 q 0,-2.508 1.518,-4.092 1.518,-1.584 3.861,-1.584 z m 17.88599,15.675 q 0,3.399 -2.112,5.544 -2.079,2.145 -5.511,2.145 -3.366,0 -5.346,-2.079 -1.947,-2.079 -1.947,-5.478 0,-3.234 1.881,-5.544 1.881,-2.31 5.61,-2.31 3.729,0 5.577,2.31 1.848,2Line truncated
|
||||
id="text51"
|
||||
style="font-weight:500;font-size:33px;fill:#c2cad6"
|
||||
aria-label="A clean start for WordPress plugins, made with aidevops." />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 42 KiB |
@@ -0,0 +1,360 @@
|
||||
<?php
|
||||
/**
|
||||
* WP Plugin Starter admin screen.
|
||||
*
|
||||
* Owns the Settings → WP Plugin Starter page: tab registry, page chrome and the
|
||||
* single admin script/stylesheet. Tab content is delegated to the manager
|
||||
* classes. Settings tabs and header links come from WPStarter_Setup; add
|
||||
* other tabs with the `wpstarter_admin_tabs` filter.
|
||||
*
|
||||
* @package WPStarter
|
||||
* @since 0.2.0
|
||||
*/
|
||||
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
class WPStarter_Admin_Manager {
|
||||
|
||||
/** Menu/page slug. */
|
||||
const PAGE = 'wp-plugin-starter-template';
|
||||
|
||||
/** Hook suffix returned by add_options_page(). */
|
||||
const HOOK = 'settings_page_' . self::PAGE;
|
||||
|
||||
/**
|
||||
* Register hooks (once).
|
||||
*/
|
||||
public static function init() {
|
||||
add_action('admin_menu', array(__CLASS__, 'register_admin_menu'));
|
||||
add_action('admin_enqueue_scripts', array(__CLASS__, 'enqueue_assets'));
|
||||
add_filter('plugin_action_links_' . plugin_basename(WPSTARTER_FILE), array(__CLASS__, 'plugin_action_links'));
|
||||
}
|
||||
|
||||
/** Slug of the search results screen (not shown in the navigation). */
|
||||
const SEARCH = 'search';
|
||||
|
||||
/**
|
||||
* Settings tabs, in navigation order (WPStarter_Setup::settings_tabs()).
|
||||
*
|
||||
* @return array<string,array{label:string,description:string}>
|
||||
*/
|
||||
public static function settings_tabs() {
|
||||
return WPStarter_Setup::settings_tabs();
|
||||
}
|
||||
|
||||
/**
|
||||
* Registered tabs.
|
||||
*
|
||||
* @return array<string,array{label:string,group:string,render:callable}>
|
||||
*/
|
||||
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('<a href="%s">%s</a>', 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'),
|
||||
);
|
||||
?>
|
||||
<div class="wrap wps-wrap">
|
||||
<header class="wps-header">
|
||||
<div class="wps-header__brand">
|
||||
<span class="wps-header__logo dashicons dashicons-star-filled" aria-hidden="true"></span>
|
||||
<h1 class="wps-header__title"><?php esc_html_e('WP Plugin Starter', 'wp-plugin-starter-template'); ?></h1>
|
||||
<span class="wps-badge"><?php echo esc_html('v' . WPSTARTER_VERSION); ?></span>
|
||||
</div>
|
||||
<form class="wps-search" role="search" method="get" action="<?php echo esc_url(admin_url('options-general.php')); ?>">
|
||||
<input type="hidden" name="page" value="<?php echo esc_attr(self::PAGE); ?>" />
|
||||
<input type="hidden" name="tab" value="<?php echo esc_attr(self::SEARCH); ?>" />
|
||||
<label class="screen-reader-text" for="wps-search-input"><?php esc_html_e('Search features', 'wp-plugin-starter-template'); ?></label>
|
||||
<span class="wps-search__field">
|
||||
<span class="wps-search__icon dashicons dashicons-search" aria-hidden="true"></span>
|
||||
<input type="search"
|
||||
id="wps-search-input"
|
||||
class="wps-search__input"
|
||||
name="s"
|
||||
maxlength="100"
|
||||
value="<?php echo esc_attr(self::search_query()); ?>"
|
||||
placeholder="<?php esc_attr_e('Search features', 'wp-plugin-starter-template'); ?>" />
|
||||
</span>
|
||||
<button type="submit" class="button wps-search__button"><?php esc_html_e('Search', 'wp-plugin-starter-template'); ?></button>
|
||||
</form>
|
||||
<div class="wps-header__actions">
|
||||
<?php if (!empty($links['website'])) : ?>
|
||||
<a class="button" href="<?php echo esc_url($links['website']); ?>" target="_blank" rel="noopener noreferrer">
|
||||
<?php esc_html_e('Visit website', 'wp-plugin-starter-template'); ?>
|
||||
<span class="screen-reader-text"><?php esc_html_e('(opens in a new tab)', 'wp-plugin-starter-template'); ?></span>
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
<?php if (!empty($links['support'])) : ?>
|
||||
<a class="button wps-header__support" href="<?php echo esc_url($links['support']); ?>" target="_blank" rel="noopener noreferrer">
|
||||
<span class="dashicons dashicons-sos" aria-hidden="true"></span>
|
||||
<?php esc_html_e('Report a problem', 'wp-plugin-starter-template'); ?>
|
||||
<span class="screen-reader-text"><?php esc_html_e('(opens in a new tab)', 'wp-plugin-starter-template'); ?></span>
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
<?php if (!empty($links['donate'])) : ?>
|
||||
<a class="button wps-header__support wps-header__donate" href="<?php echo esc_url($links['donate']); ?>" target="_blank" rel="noopener noreferrer">
|
||||
<span class="dashicons dashicons-coffee" aria-hidden="true"></span>
|
||||
<?php esc_html_e('Buy me a coffee', 'wp-plugin-starter-template'); ?>
|
||||
<span class="screen-reader-text"><?php esc_html_e('(opens in a new tab)', 'wp-plugin-starter-template'); ?></span>
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<nav class="wps-nav" aria-label="<?php esc_attr_e('WP Plugin Starter sections', 'wp-plugin-starter-template'); ?>">
|
||||
<?php foreach ($groups as $group => $group_label) : ?>
|
||||
<?php
|
||||
$group_tabs = array_filter($tabs, function ($tab) use ($group) {
|
||||
return isset($tab['group']) && $tab['group'] === $group;
|
||||
});
|
||||
if (!$group_tabs) {
|
||||
continue;
|
||||
}
|
||||
?>
|
||||
<div class="wps-nav__group" role="group" aria-label="<?php echo esc_attr($group_label); ?>">
|
||||
<?php foreach ($group_tabs as $slug => $tab) : ?>
|
||||
<a href="<?php echo esc_url(self::tab_url($slug)); ?>"
|
||||
class="wps-nav__tab<?php echo $slug === $active ? ' is-active' : ''; ?>"
|
||||
<?php echo $slug === $active ? 'aria-current="page"' : ''; ?>>
|
||||
<?php echo esc_html($tab['label']); ?>
|
||||
</a>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</nav>
|
||||
|
||||
<?php // Core moves admin notices after this marker instead of into the header. ?>
|
||||
<hr class="wp-header-end" />
|
||||
|
||||
<?php settings_errors(); ?>
|
||||
|
||||
<?php // Not <main>: core's #wpbody already carries role="main". ?>
|
||||
<div class="wps-main wps-tab-<?php echo esc_attr($active); ?>" id="wps-tab-<?php echo esc_attr($active); ?>">
|
||||
<?php
|
||||
if (self::SEARCH === $active) {
|
||||
WPStarter_Settings_Manager::render_search(self::search_query(), $tabs);
|
||||
} elseif (isset($tabs[$active]['render']) && is_callable($tabs[$active]['render'])) {
|
||||
call_user_func($tabs[$active]['render']);
|
||||
}
|
||||
?>
|
||||
</div>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,213 @@
|
||||
<?php
|
||||
/**
|
||||
* WP Plugin Starter Read Me tab.
|
||||
*
|
||||
* Renders README.md with a small, escaping Markdown subset
|
||||
* (headings with GitHub-style IDs, lists, tables, bold, italic, inline code,
|
||||
* http(s) links, links to headings and images from the plugin's folder).
|
||||
*
|
||||
* @package WPStarter
|
||||
* @since 0.2.0
|
||||
*/
|
||||
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
class WPStarter_Readme_Manager {
|
||||
|
||||
/**
|
||||
* Render the tab.
|
||||
*/
|
||||
public static function display_tab_content() {
|
||||
$readme = wpstarter_get_readme_content();
|
||||
?>
|
||||
<article class="wps-card wps-readme">
|
||||
<?php echo self::parse_markdown($readme['content']); // phpcs:ignore WordPress.Security.EscapeOutput -- built from escaped text. ?>
|
||||
</article>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert Markdown to HTML. Input is escaped first; only the generated
|
||||
* tags and validated http(s) links are emitted.
|
||||
*
|
||||
* @param string $markdown Markdown.
|
||||
* @return string HTML.
|
||||
*/
|
||||
public static function parse_markdown($markdown) {
|
||||
$markdown = str_replace('{WPSTARTER_VERSION}', WPSTARTER_VERSION, $markdown);
|
||||
$lines = preg_split('/\r\n|\r|\n/', $markdown);
|
||||
$html = '';
|
||||
$list = '';
|
||||
$table = ''; // '', 'head' (header row written) or 'body'.
|
||||
$ids = array();
|
||||
|
||||
$close_list = function () use (&$html, &$list, &$table) {
|
||||
if ($list) {
|
||||
$html .= '</' . $list . '>';
|
||||
$list = '';
|
||||
}
|
||||
if ($table) {
|
||||
$html .= ('head' === $table ? '</thead>' : '</tbody>') . '</table></div>';
|
||||
$table = '';
|
||||
}
|
||||
};
|
||||
|
||||
foreach ($lines as $line) {
|
||||
$trim = trim($line);
|
||||
|
||||
if ('' === $trim || '#' === $trim) {
|
||||
$close_list();
|
||||
continue;
|
||||
}
|
||||
|
||||
// An image on a line of its own, from the plugin's own folder.
|
||||
if (preg_match('/^!\[([^\]]*)\]\(([^)\s]+)\)$/', $trim, $m)) {
|
||||
$close_list();
|
||||
$html .= self::image($m[2], $m[1]);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (preg_match('/^(#{1,4})\s+(.+)$/', $trim, $m)) {
|
||||
$close_list();
|
||||
$level = min(4, strlen($m[1]) + 1); // h1 is reserved for the page title.
|
||||
$html .= sprintf('<h%1$d id="%2$s">%3$s</h%1$d>', $level, esc_attr(self::anchor($m[2], $ids)), self::inline($m[2]));
|
||||
continue;
|
||||
}
|
||||
|
||||
// Tables: a header row, a |---| separator, then body rows.
|
||||
if (strlen($trim) > 1 && '|' === $trim[0] && '|' === substr($trim, -1)) {
|
||||
if ('head' === $table && preg_match('/^\|[\s:|-]+\|$/', $trim)) {
|
||||
$html .= '</thead><tbody>';
|
||||
$table = 'body';
|
||||
continue;
|
||||
}
|
||||
if (!$table) {
|
||||
$close_list();
|
||||
$html .= '<div class="wps-readme-table"><table><thead>';
|
||||
$table = 'head';
|
||||
}
|
||||
$tag = 'head' === $table ? 'th' : 'td';
|
||||
$html .= '<tr>';
|
||||
foreach (explode('|', substr($trim, 1, -1)) as $cell) {
|
||||
$html .= '<' . $tag . '>' . self::inline(trim($cell)) . '</' . $tag . '>';
|
||||
}
|
||||
$html .= '</tr>';
|
||||
continue;
|
||||
}
|
||||
|
||||
if (preg_match('/^[-*]\s+(.+)$/', $trim, $m) || preg_match('/^\d+\.\s+(.+)$/', $trim, $n)) {
|
||||
$type = isset($n[1]) ? 'ol' : 'ul';
|
||||
$text = isset($n[1]) ? $n[1] : $m[1];
|
||||
if ($list !== $type) {
|
||||
$close_list();
|
||||
$html .= '<' . $type . '>';
|
||||
$list = $type;
|
||||
}
|
||||
$html .= '<li>' . self::inline($text) . '</li>';
|
||||
unset($n);
|
||||
continue;
|
||||
}
|
||||
|
||||
$close_list();
|
||||
$html .= '<p>' . self::inline($trim) . '</p>';
|
||||
}
|
||||
$close_list();
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Heading ID as GitHub makes it, so README links such as
|
||||
* `[Credits](#credits)` work in the tab too. Repeats get -1, -2...
|
||||
*
|
||||
* @param string $text Heading Markdown.
|
||||
* @param array $ids IDs used so far (updated).
|
||||
* @return string ID.
|
||||
*/
|
||||
private static function anchor($text, array &$ids) {
|
||||
$text = preg_replace('/\[([^\]]+)\]\([^)]*\)/', '$1', $text);
|
||||
$text = str_replace(array('`', '*'), '', $text);
|
||||
$id = function_exists('mb_strtolower') ? mb_strtolower($text, 'UTF-8') : strtolower($text);
|
||||
$id = preg_replace('/[^\p{L}\p{N}\s_-]/u', '', $id);
|
||||
$id = preg_replace('/\s/u', '-', trim($id));
|
||||
$base = '' === $id ? 'section' : $id;
|
||||
$id = $base;
|
||||
$n = 0;
|
||||
while (isset($ids[$id])) {
|
||||
$id = $base . '-' . (++$n);
|
||||
}
|
||||
$ids[$id] = true;
|
||||
|
||||
return $id;
|
||||
}
|
||||
|
||||
/**
|
||||
* An image from the plugin's folder, such as the banner at the top of
|
||||
* README.md. Only relative paths to image files that exist are shown, so
|
||||
* the tab never loads anything from another site.
|
||||
*
|
||||
* @param string $path Path relative to the plugin folder.
|
||||
* @param string $alt Alternative text.
|
||||
* @return string HTML, or '' for anything else.
|
||||
*/
|
||||
private static function image($path, $alt) {
|
||||
if (!preg_match('#^[A-Za-z0-9_-]+(?:/[A-Za-z0-9_-]+)*\.(?:svg|png|jpe?g|gif|webp)$#i', $path)) {
|
||||
return '';
|
||||
}
|
||||
$file = WPSTARTER_DIR . $path;
|
||||
if (!is_file($file)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Width and height stop the page jumping while the image loads.
|
||||
$size = '';
|
||||
if ('svg' === strtolower(pathinfo($path, PATHINFO_EXTENSION))) {
|
||||
$head = (string) file_get_contents($file, false, null, 0, 2048); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- local file.
|
||||
if (preg_match('/<svg\b[^>]*?\swidth="(\d+)"[^>]*?\sheight="(\d+)"/', $head, $m)) {
|
||||
$size = array((int) $m[1], (int) $m[2]);
|
||||
}
|
||||
} else {
|
||||
$size = wp_getimagesize($file);
|
||||
}
|
||||
$dims = is_array($size) && !empty($size[0]) && !empty($size[1])
|
||||
? sprintf(' width="%d" height="%d"', $size[0], $size[1])
|
||||
: '';
|
||||
|
||||
// The file's time in the address, so browsers fetch a changed image
|
||||
// (such as a new banner after an update) instead of a cached one.
|
||||
$url = add_query_arg('ver', (string) filemtime($file), WPSTARTER_URL . $path);
|
||||
|
||||
return sprintf(
|
||||
'<p class="wps-readme-image"><img src="%1$s" alt="%2$s"%3$s decoding="async" /></p>',
|
||||
esc_url($url),
|
||||
esc_attr($alt),
|
||||
$dims
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Inline Markdown on escaped text.
|
||||
*
|
||||
* @param string $text Raw text.
|
||||
* @return string HTML.
|
||||
*/
|
||||
private static function inline($text) {
|
||||
$text = esc_html($text);
|
||||
$text = preg_replace('/`([^`]+)`/', '<code>$1</code>', $text);
|
||||
$text = preg_replace('/\*\*(.+?)\*\*/', '<strong>$1</strong>', $text);
|
||||
$text = preg_replace('/(?<![*\w])\*(?!\s)(.+?)(?<!\s)\*(?![*\w])/', '<em>$1</em>', $text);
|
||||
|
||||
return preg_replace_callback('/\[([^\]]+)\]\(([^)\s]+)\)/', function ($m) {
|
||||
$url = esc_url(html_entity_decode($m[2]), array('http', 'https'));
|
||||
if ('#' === $m[2][0]) {
|
||||
return sprintf('<a href="#%1$s">%2$s</a>', esc_attr(substr($m[2], 1)), $m[1]);
|
||||
}
|
||||
if ('' === $url) {
|
||||
return $m[1];
|
||||
}
|
||||
return sprintf('<a href="%1$s" target="_blank" rel="noopener noreferrer">%2$s</a>', $url, $m[1]);
|
||||
}, $text);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,431 @@
|
||||
<?php
|
||||
/**
|
||||
* Plugins screen: plugins WP Plugin Starter can replace.
|
||||
*
|
||||
* Lists installed plugins that a setting's `replaces` names:
|
||||
* - active, with every setting that replaces it on: the plugin can go, with a
|
||||
* Deactivate link (the settings wait until it is deactivated);
|
||||
* - active, with a replacing setting off: WP Plugin Starter can do this job, with
|
||||
* a link to the settings (only for people who can change them);
|
||||
* - installed but inactive, with every replacing setting on: no longer
|
||||
* needed, with a Delete link (single sites; on multisite a plugin may be
|
||||
* active on another site).
|
||||
* While an active plugin does things here that WP Plugin Starter does not (the
|
||||
* `wpstarter_replaced_plugin_extras` filter), the notice names them
|
||||
* instead of saying the plugin can go.
|
||||
*
|
||||
* Each of those plugins, and inactive ones whose replacing setting is off,
|
||||
* also gets a note under its own row with the same step.
|
||||
*
|
||||
* Shown on the Plugins screen to people who can activate plugins. "Hide"
|
||||
* hides the plugins listed at the time for that person; a plugin that needs
|
||||
* a different step later shows again.
|
||||
*
|
||||
* @package WPStarter
|
||||
* @since 0.7.0
|
||||
*/
|
||||
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
class WPStarter_Replaced_Plugins {
|
||||
|
||||
/** admin-post action, also the nonce action. */
|
||||
const HIDE = 'wpstarter_hide_replaced_plugins';
|
||||
|
||||
/** User meta: list of "slug:step" items the person hid. */
|
||||
const HIDDEN = 'wpstarter_replaced_plugins_hidden';
|
||||
|
||||
/** admin-post action that deactivates a replaced plugin and comes back. */
|
||||
const DEACTIVATE = 'wpstarter_deactivate_replaced';
|
||||
|
||||
/** Query argument on the page it comes back to. */
|
||||
const DONE = 'wpstarter-deactivated';
|
||||
|
||||
/**
|
||||
* Register hooks.
|
||||
*/
|
||||
public static function init() {
|
||||
add_action('load-plugins.php', array(__CLASS__, 'load_screen'));
|
||||
add_action('admin_post_' . self::HIDE, array(__CLASS__, 'hide'));
|
||||
add_action('admin_post_' . self::DEACTIVATE, array(__CLASS__, 'deactivate'));
|
||||
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- only shows a message.
|
||||
if (isset($_GET[self::DONE])) {
|
||||
add_action('admin_notices', array(__CLASS__, 'deactivated_notice'));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Plugins screen: add the notice and the notes on each plugin's row.
|
||||
*/
|
||||
public static function load_screen() {
|
||||
if (!current_user_can('activate_plugins')) {
|
||||
return;
|
||||
}
|
||||
add_action(is_network_admin() ? 'network_admin_notices' : 'admin_notices', array(__CLASS__, 'notice'));
|
||||
add_action('after_plugin_row', array(__CLASS__, 'row_note'), 10, 1);
|
||||
add_action('admin_head', array(__CLASS__, 'row_note_style'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Join a row note to its plugin's row, as core does for update notes.
|
||||
*/
|
||||
public static function row_note_style() {
|
||||
echo '<style>.plugins tr:has(+ tr.wps-replaced-row) th, .plugins tr:has(+ tr.wps-replaced-row) td { box-shadow: none; }</style>' . "\n";
|
||||
}
|
||||
|
||||
/**
|
||||
* Link that deactivates a replaced plugin on this site and comes back to
|
||||
* the page it was clicked on.
|
||||
*
|
||||
* @param string $file Plugin file.
|
||||
* @return string Unescaped URL.
|
||||
*/
|
||||
public static function deactivate_url($file) {
|
||||
return wp_nonce_url(add_query_arg(array(
|
||||
'action' => self::DEACTIVATE,
|
||||
'plugin' => rawurlencode($file),
|
||||
'_wp_http_referer' => rawurlencode(remove_query_arg(array(self::DONE, 'deactivate', 'activate'))),
|
||||
), admin_url('admin-post.php')), self::DEACTIVATE . '_' . $file);
|
||||
}
|
||||
|
||||
/**
|
||||
* Core's deactivate link on the Plugins screen, keeping the list's
|
||||
* status, page and search, so it comes back to the same list.
|
||||
*
|
||||
* @param string $file Plugin file.
|
||||
* @return string Unescaped URL.
|
||||
*/
|
||||
private static function plugins_deactivate_url($file) {
|
||||
// phpcs:disable WordPress.Security.NonceVerification.Recommended -- the list being shown.
|
||||
$args = array(
|
||||
'action' => 'deactivate',
|
||||
'plugin' => rawurlencode($file),
|
||||
'plugin_status' => isset($_GET['plugin_status']) ? sanitize_key(wp_unslash($_GET['plugin_status'])) : 'all',
|
||||
'paged' => isset($_GET['paged']) ? absint($_GET['paged']) : 1,
|
||||
's' => isset($_GET['s']) ? rawurlencode(sanitize_text_field(wp_unslash($_GET['s']))) : '',
|
||||
);
|
||||
// phpcs:enable
|
||||
return wp_nonce_url(add_query_arg($args, self_admin_url('plugins.php')), 'deactivate-plugin_' . $file);
|
||||
}
|
||||
|
||||
/**
|
||||
* admin-post: deactivate a replaced plugin on this site, then go back.
|
||||
*/
|
||||
public static function deactivate() {
|
||||
$file = isset($_GET['plugin']) ? sanitize_text_field(wp_unslash($_GET['plugin'])) : '';
|
||||
check_admin_referer(self::DEACTIVATE . '_' . $file);
|
||||
if ('' === $file || validate_file($file) || !current_user_can('deactivate_plugin', $file)) {
|
||||
wp_die(esc_html__('You are not allowed to deactivate this plugin.', 'wp-plugin-starter-template'), '', array('response' => 403));
|
||||
}
|
||||
if (!isset(self::replaced()[dirname($file)])) {
|
||||
wp_die(esc_html__('WP Plugin Starter only deactivates plugins it replaces.', 'wp-plugin-starter-template'), '', array('response' => 400));
|
||||
}
|
||||
if (!function_exists('is_plugin_active')) {
|
||||
require_once ABSPATH . 'wp-admin/includes/plugin.php';
|
||||
}
|
||||
$back = wp_get_referer();
|
||||
$back = $back ? $back : admin_url('plugins.php');
|
||||
// Network-wide plugins are deactivated in the network admin.
|
||||
if (is_plugin_active($file) && !is_plugin_active_for_network($file)) {
|
||||
deactivate_plugins($file);
|
||||
update_option('recently_activated', array($file => time()) + (array) get_option('recently_activated'), false);
|
||||
$back = add_query_arg(self::DONE, 1, $back);
|
||||
}
|
||||
wp_safe_redirect($back);
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* "Plugin deactivated." on the page the link came back to.
|
||||
*/
|
||||
public static function deactivated_notice() {
|
||||
echo '<div class="notice notice-success is-dismissible"><p>' . esc_html__('Plugin deactivated.', 'wp-plugin-starter-template') . '</p></div>';
|
||||
}
|
||||
|
||||
/**
|
||||
* A note under a replaced plugin's row on the Plugins screen: which
|
||||
* setting does its job and what to do next.
|
||||
*
|
||||
* @param string $file Plugin file.
|
||||
*/
|
||||
public static function row_note($file) {
|
||||
global $wp_list_table;
|
||||
$states = self::states();
|
||||
if (!isset($states[$file])) {
|
||||
return;
|
||||
}
|
||||
$item = $states[$file];
|
||||
$labels = esc_html(implode(', ', array_map(function ($label) {
|
||||
return '‘' . $label . '’';
|
||||
}, $item['settings'])));
|
||||
$setting = '';
|
||||
if (WPStarter_Settings::can_change()) {
|
||||
$setting = sprintf(' <a href="%1$s">%2$s</a>', esc_url(WPStarter_Admin_Manager::tab_url(WPStarter_Admin_Manager::SEARCH, array('s' => $item['name']))), esc_html__('Show the setting', 'wp-plugin-starter-template'));
|
||||
}
|
||||
$extras = esc_html(implode(', ', $item['extras']));
|
||||
|
||||
switch ($item['step']) {
|
||||
case 'deactivate':
|
||||
/* translators: %s: WP Plugin Starter setting names */
|
||||
$text = sprintf(esc_html__('WP Plugin Starter makes this plugin redundant: %s is on and takes over once you deactivate this plugin. Then you can delete it.', 'wp-plugin-starter-template'), $labels);
|
||||
break;
|
||||
case 'partial':
|
||||
/* translators: 1: WP Plugin Starter setting names, 2: the plugin's settings WP Plugin Starter does not have */
|
||||
$text = sprintf(esc_html__('WP Plugin Starter can do this plugin\'s job: %1$s is on and takes over once you deactivate it, except for these, which WP Plugin Starter does not do: %2$s.', 'wp-plugin-starter-template'), $labels, $extras);
|
||||
break;
|
||||
case 'switch_on':
|
||||
/* translators: %s: WP Plugin Starter setting names */
|
||||
$text = sprintf(esc_html__('WP Plugin Starter makes this plugin redundant: turn on %s in WP Plugin Starter, then deactivate and delete this plugin.', 'wp-plugin-starter-template'), $labels) . $setting;
|
||||
break;
|
||||
case 'switch_on_partial':
|
||||
/* translators: 1: WP Plugin Starter setting names, 2: the plugin's settings WP Plugin Starter does not have */
|
||||
$text = sprintf(esc_html__('WP Plugin Starter can do part of this plugin\'s job with %1$s. It does not do these, which this plugin has on: %2$s.', 'wp-plugin-starter-template'), $labels, $extras) . $setting;
|
||||
break;
|
||||
case 'switch_on_inactive':
|
||||
/* translators: %s: WP Plugin Starter setting names */
|
||||
$text = sprintf(esc_html__('WP Plugin Starter makes this plugin redundant: turn on %s in WP Plugin Starter, then you can delete this plugin.', 'wp-plugin-starter-template'), $labels) . $setting;
|
||||
break;
|
||||
default:
|
||||
/* translators: %s: WP Plugin Starter setting names */
|
||||
$text = sprintf(esc_html__('No longer needed: %s in WP Plugin Starter does this plugin\'s job. You can delete it.', 'wp-plugin-starter-template'), $labels);
|
||||
}
|
||||
$columns = ($wp_list_table instanceof WP_List_Table) ? $wp_list_table->get_column_count() : 4;
|
||||
$active = in_array($item['step'], array('deactivate', 'partial', 'switch_on', 'switch_on_partial'), true) ? ' active' : ' inactive';
|
||||
printf(
|
||||
'<tr class="plugin-update-tr wps-replaced-row%1$s"><td colspan="%2$d" class="plugin-update colspanchange"><div class="notice inline notice-info notice-alt"><p>%3$s</p></div></td></tr>',
|
||||
esc_attr($active),
|
||||
(int) $columns,
|
||||
$text // phpcs:ignore WordPress.Security.EscapeOutput -- built from escaped parts above.
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaced plugins, keyed by folder name, with the settings that replace
|
||||
* them.
|
||||
*
|
||||
* @return array<string,array{name:string,settings:array<string,string>}> slug => name and setting key => label.
|
||||
*/
|
||||
private static function replaced() {
|
||||
$replaced = array();
|
||||
foreach (WPStarter_Settings::schema() as $key => $field) {
|
||||
if (empty($field['replaces']) || !is_array($field['replaces']) || !empty($field['parent'])) {
|
||||
continue;
|
||||
}
|
||||
foreach ($field['replaces'] as $slug => $name) {
|
||||
if (!isset($replaced[$slug])) {
|
||||
$replaced[$slug] = array('name' => (string) $name, 'settings' => array());
|
||||
}
|
||||
$replaced[$slug]['settings'][$key] = isset($field['label']) ? (string) $field['label'] : (string) $key;
|
||||
}
|
||||
}
|
||||
return $replaced;
|
||||
}
|
||||
|
||||
/**
|
||||
* What to show in the notice: one item per installed replaced plugin that
|
||||
* needs a step, leaving out what the person hid.
|
||||
*
|
||||
* @return array<int,array{slug:string,file:string,name:string,step:string,settings:array<string,string>}>
|
||||
*/
|
||||
private static function items() {
|
||||
$items = array_filter(self::states(), function ($item) {
|
||||
// Inactive with the setting off: only a note on its row.
|
||||
return 'switch_on_inactive' !== $item['step']
|
||||
&& ('switch_on' !== $item['step'] || WPStarter_Settings::can_change());
|
||||
});
|
||||
$hidden = (array) get_user_meta(get_current_user_id(), self::HIDDEN, true);
|
||||
return array_values(array_filter($items, function ($item) use ($hidden) {
|
||||
return !in_array($item['slug'] . ':' . $item['step'], $hidden, true);
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Every installed replaced plugin that needs a step, keyed by plugin file.
|
||||
*
|
||||
* Steps: deactivate, partial (active, setting on, it does more),
|
||||
* switch_on, switch_on_partial, delete (inactive, setting on) and
|
||||
* switch_on_inactive (inactive, setting off; single sites only).
|
||||
*
|
||||
* @return array<string,array{slug:string,file:string,name:string,step:string,settings:array<string,string>,extras:string[]}>
|
||||
*/
|
||||
private static function states() {
|
||||
static $states = null;
|
||||
if (null !== $states) {
|
||||
return $states;
|
||||
}
|
||||
if (!function_exists('get_plugins')) {
|
||||
require_once ABSPATH . 'wp-admin/includes/plugin.php';
|
||||
}
|
||||
$installed = array();
|
||||
foreach (array_keys(get_plugins()) as $file) {
|
||||
$slug = dirname($file);
|
||||
if ('.' !== $slug) {
|
||||
$installed[$slug] = $file;
|
||||
}
|
||||
}
|
||||
|
||||
$network = is_multisite() && is_network_admin();
|
||||
// Active where this screen can deactivate it: network-wide in the
|
||||
// network admin, on this site otherwise. Stored lists, so plugins
|
||||
// skipped on this screen still count.
|
||||
$here = $network
|
||||
? array_keys((array) get_site_option('active_sitewide_plugins', array()))
|
||||
: WPStarter_Feature::stored_active_plugins();
|
||||
$all = WPStarter_Feature::active_plugins();
|
||||
|
||||
$items = array();
|
||||
foreach (self::replaced() as $slug => $plugin) {
|
||||
if (!isset($installed[$slug])) {
|
||||
continue;
|
||||
}
|
||||
$file = $installed[$slug];
|
||||
$all_on = true;
|
||||
foreach (array_keys($plugin['settings']) as $key) {
|
||||
$all_on = $all_on && (bool) WPStarter_Settings::get($key);
|
||||
}
|
||||
|
||||
if (in_array($file, $here, true)) {
|
||||
$step = $all_on ? 'deactivate' : 'switch_on';
|
||||
} elseif (!isset($all[$slug]) && !is_multisite()) {
|
||||
$step = $all_on ? 'delete' : 'switch_on_inactive';
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
$extras = array();
|
||||
if ('deactivate' === $step || 'switch_on' === $step) {
|
||||
/**
|
||||
* What a replaced plugin does on this site that WP Plugin Starter,
|
||||
* as set up, does not. When there is any, the notice lists it
|
||||
* instead of saying the plugin can go.
|
||||
*
|
||||
* @param string[] $extras Plain names.
|
||||
* @param string $slug Plugin folder.
|
||||
*/
|
||||
$extras = array_values(array_filter(array_map('strval', (array) apply_filters('wpstarter_replaced_plugin_extras', array(), (string) $slug))));
|
||||
}
|
||||
if ($extras) {
|
||||
$step = 'deactivate' === $step ? 'partial' : 'switch_on_partial';
|
||||
}
|
||||
$items[$file] = array(
|
||||
'slug' => (string) $slug,
|
||||
'file' => $file,
|
||||
'name' => $plugin['name'],
|
||||
'step' => $step,
|
||||
'settings' => $plugin['settings'],
|
||||
'extras' => $extras,
|
||||
);
|
||||
}
|
||||
$states = $items;
|
||||
return $states;
|
||||
}
|
||||
|
||||
/**
|
||||
* The notice.
|
||||
*/
|
||||
public static function notice() {
|
||||
$items = self::items();
|
||||
if (!$items) {
|
||||
return;
|
||||
}
|
||||
$rows = array();
|
||||
foreach ($items as $item) {
|
||||
$rows[] = self::row($item);
|
||||
}
|
||||
$keys = array_map(function ($item) {
|
||||
return $item['slug'] . ':' . $item['step'];
|
||||
}, $items);
|
||||
$hide = wp_nonce_url(add_query_arg(array('action' => self::HIDE, 'items' => implode(',', $keys)), admin_url('admin-post.php')), self::HIDE);
|
||||
?>
|
||||
<div class="notice notice-info wps-replaced-plugins">
|
||||
<p><strong><?php esc_html_e('WP Plugin Starter can do the job of these plugins:', 'wp-plugin-starter-template'); ?></strong></p>
|
||||
<ul class="ul-disc">
|
||||
<?php foreach ($rows as $row) : ?>
|
||||
<li><?php echo $row; // phpcs:ignore WordPress.Security.EscapeOutput -- built from escaped parts in row(). ?></li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
<p><a href="<?php echo esc_url($hide); ?>"><?php esc_html_e('Hide', 'wp-plugin-starter-template'); ?></a></p>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* One line of the notice.
|
||||
*
|
||||
* @param array $item Item from items().
|
||||
* @return string HTML.
|
||||
*/
|
||||
private static function row(array $item) {
|
||||
$labels = implode(', ', array_map(function ($label) {
|
||||
return '‘' . $label . '’';
|
||||
}, $item['settings']));
|
||||
$name = '<strong>' . esc_html($item['name']) . '</strong>';
|
||||
$base = self_admin_url('plugins.php');
|
||||
|
||||
if ('deactivate' === $item['step']) {
|
||||
/* translators: 1: plugin name, 2: WP Plugin Starter setting names */
|
||||
$text = sprintf(esc_html__('%1$s: %2$s is on and takes over once you deactivate it. Then you can delete it.', 'wp-plugin-starter-template'), $name, esc_html($labels));
|
||||
if (current_user_can('deactivate_plugin', $item['file'])) {
|
||||
$url = self::plugins_deactivate_url($item['file']);
|
||||
/* translators: %s: plugin name */
|
||||
$text .= sprintf(' <a href="%1$s">%2$s</a>', esc_url($url), esc_html(sprintf(__('Deactivate %s', 'wp-plugin-starter-template'), $item['name'])));
|
||||
}
|
||||
return $text;
|
||||
}
|
||||
|
||||
if ('partial' === $item['step'] || 'switch_on_partial' === $item['step']) {
|
||||
$extras = esc_html(implode(', ', $item['extras']));
|
||||
if ('partial' === $item['step']) {
|
||||
/* translators: 1: plugin name, 2: WP Plugin Starter setting names, 3: the plugin's settings WP Plugin Starter does not have */
|
||||
$text = sprintf(esc_html__('%1$s: %2$s is on and takes over once you deactivate it, except for these, which WP Plugin Starter does not do: %3$s. Deactivate it only if you no longer need them.', 'wp-plugin-starter-template'), $name, esc_html($labels), $extras);
|
||||
if (current_user_can('deactivate_plugin', $item['file'])) {
|
||||
$url = self::plugins_deactivate_url($item['file']);
|
||||
/* translators: %s: plugin name */
|
||||
$text .= sprintf(' <a href="%1$s">%2$s</a>', esc_url($url), esc_html(sprintf(__('Deactivate %s', 'wp-plugin-starter-template'), $item['name'])));
|
||||
}
|
||||
return $text;
|
||||
}
|
||||
/* translators: 1: plugin name, 2: WP Plugin Starter setting names, 3: the plugin's settings WP Plugin Starter does not have */
|
||||
$text = sprintf(esc_html__('%1$s: %2$s can do part of its job. WP Plugin Starter does not do these, which it has on: %3$s.', 'wp-plugin-starter-template'), $name, esc_html($labels), $extras);
|
||||
$url = WPStarter_Admin_Manager::tab_url(WPStarter_Admin_Manager::SEARCH, array('s' => $item['name']));
|
||||
return $text . sprintf(' <a href="%1$s">%2$s</a>', esc_url($url), esc_html__('Show the setting', 'wp-plugin-starter-template'));
|
||||
}
|
||||
|
||||
if ('delete' === $item['step']) {
|
||||
/* translators: 1: plugin name, 2: WP Plugin Starter setting names */
|
||||
$text = sprintf(esc_html__('%1$s is inactive and no longer needed: %2$s does its job.', 'wp-plugin-starter-template'), $name, esc_html($labels));
|
||||
if (current_user_can('delete_plugins')) {
|
||||
$url = wp_nonce_url(add_query_arg(array('action' => 'delete-selected', 'checked[]' => $item['file'], 'plugin_status' => 'all'), $base), 'bulk-plugins');
|
||||
/* translators: %s: plugin name */
|
||||
$text .= sprintf(' <a href="%1$s">%2$s</a>', esc_url($url), esc_html(sprintf(__('Delete %s', 'wp-plugin-starter-template'), $item['name'])));
|
||||
}
|
||||
return $text;
|
||||
}
|
||||
|
||||
/* translators: 1: plugin name, 2: WP Plugin Starter setting names */
|
||||
$text = sprintf(esc_html__('%1$s: switch on %2$s, then deactivate and delete it.', 'wp-plugin-starter-template'), $name, esc_html($labels));
|
||||
$url = WPStarter_Admin_Manager::tab_url(WPStarter_Admin_Manager::SEARCH, array('s' => $item['name']));
|
||||
return $text . sprintf(' <a href="%1$s">%2$s</a>', esc_url($url), esc_html__('Show the setting', 'wp-plugin-starter-template'));
|
||||
}
|
||||
|
||||
/**
|
||||
* admin-post: hide the listed items for this person.
|
||||
*/
|
||||
public static function hide() {
|
||||
check_admin_referer(self::HIDE);
|
||||
if (!current_user_can('activate_plugins')) {
|
||||
wp_die(esc_html__('You are not allowed to manage plugins on this site.', 'wp-plugin-starter-template'), '', array('response' => 403));
|
||||
}
|
||||
$items = isset($_GET['items']) ? explode(',', sanitize_text_field(wp_unslash($_GET['items']))) : array();
|
||||
$items = array_filter($items, function ($item) {
|
||||
return (bool) preg_match('/^[A-Za-z0-9._-]+:(deactivate|switch_on|delete|partial|switch_on_partial)$/', $item);
|
||||
});
|
||||
$hidden = (array) get_user_meta(get_current_user_id(), self::HIDDEN, true);
|
||||
update_user_meta(get_current_user_id(), self::HIDDEN, array_values(array_unique(array_filter(array_merge($hidden, $items)))));
|
||||
|
||||
$back = wp_get_referer();
|
||||
wp_safe_redirect($back ? $back : self_admin_url('plugins.php'));
|
||||
exit;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,449 @@
|
||||
<?php
|
||||
/**
|
||||
* WP Plugin Starter settings tabs.
|
||||
*
|
||||
* Renders setting cards from WPStarter_Settings::schema(). Each top-level
|
||||
* setting is a card with a switch; child settings appear in an expandable
|
||||
* panel. Controls save instantly via AJAX (see admin/js/wpstarter-admin.js).
|
||||
*
|
||||
* @package WPStarter
|
||||
* @since 0.2.0
|
||||
*/
|
||||
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
class WPStarter_Settings_Manager {
|
||||
|
||||
/**
|
||||
* Render matching settings from every tab, grouped by tab.
|
||||
*
|
||||
* Cards are the same as on their own tab, so they can be switched on
|
||||
* and changed here.
|
||||
*
|
||||
* @param string $query Search text.
|
||||
* @param array $tabs Registered tabs (see WPStarter_Admin_Manager::get_tabs()).
|
||||
*/
|
||||
public static function render_search($query, array $tabs) {
|
||||
$groups = array();
|
||||
foreach (WPStarter_Settings::search($query) as $key => $field) {
|
||||
if (isset($tabs[$field['tab']])) {
|
||||
$groups[$field['tab']][$key] = $field;
|
||||
}
|
||||
}
|
||||
$count = array_sum(array_map('count', $groups));
|
||||
?>
|
||||
<div class="wps-section">
|
||||
<div class="wps-section__intro">
|
||||
<h2 class="wps-section__title">
|
||||
<?php
|
||||
/* translators: %s: search text */
|
||||
echo esc_html(sprintf(__('Results for “%s”', 'wp-plugin-starter-template'), $query));
|
||||
?>
|
||||
</h2>
|
||||
<p class="wps-section__desc" role="status">
|
||||
<?php
|
||||
echo esc_html($count
|
||||
/* translators: %d: number of matching features */
|
||||
? sprintf(_n('%d feature found.', '%d features found.', $count, 'wp-plugin-starter-template'), $count)
|
||||
: __('No features match. Try another word.', 'wp-plugin-starter-template'));
|
||||
?>
|
||||
</p>
|
||||
<?php if ($count) : ?>
|
||||
<p class="wps-section__hint"><?php esc_html_e('Changes are saved automatically.', 'wp-plugin-starter-template'); ?></p>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php foreach ($groups as $tab => $fields) : ?>
|
||||
<h3 class="wps-search__group">
|
||||
<a href="<?php echo esc_url(WPStarter_Admin_Manager::tab_url($tab)); ?>"><?php echo esc_html($tabs[$tab]['label']); ?></a>
|
||||
</h3>
|
||||
<div class="wps-cards">
|
||||
<?php
|
||||
foreach ($fields as $key => $field) {
|
||||
self::render_card($key, $field);
|
||||
}
|
||||
?>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Render every top-level setting for a tab.
|
||||
*
|
||||
* @param string $tab Tab slug.
|
||||
* @param string $title Section heading.
|
||||
* @param string $description Section intro.
|
||||
*/
|
||||
public static function render_tab($tab, $title, $description) {
|
||||
$fields = WPStarter_Settings::fields_for_tab($tab);
|
||||
?>
|
||||
<div class="wps-section">
|
||||
<div class="wps-section__intro">
|
||||
<h2 class="wps-section__title"><?php echo esc_html($title); ?></h2>
|
||||
<?php if ($description) : ?>
|
||||
<p class="wps-section__desc"><?php echo esc_html($description); ?></p>
|
||||
<?php endif; ?>
|
||||
<?php if ($fields) : ?>
|
||||
<p class="wps-section__hint"><?php esc_html_e('Changes are saved automatically.', 'wp-plugin-starter-template'); ?></p>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php if ($fields) : ?>
|
||||
<div class="wps-cards">
|
||||
<?php
|
||||
foreach ($fields as $key => $field) {
|
||||
self::render_card($key, $field);
|
||||
}
|
||||
?>
|
||||
</div>
|
||||
<?php else : ?>
|
||||
<div class="wps-card wps-empty">
|
||||
<p><?php esc_html_e('No settings yet.', 'wp-plugin-starter-template'); ?></p>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php
|
||||
/**
|
||||
* Fires after a settings tab's cards, for sections that are not
|
||||
* settings (such as example data).
|
||||
*
|
||||
* @param string $tab Tab slug.
|
||||
*/
|
||||
do_action('wpstarter_settings_tab_after', $tab);
|
||||
?>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a setting card.
|
||||
*
|
||||
* @param string $key Setting key.
|
||||
* @param array $field Schema entry.
|
||||
*/
|
||||
public static function render_card($key, array $field) {
|
||||
// Hidden options are wiring set by starter data or code, not choices.
|
||||
$children = array_filter(WPStarter_Settings::children_of($key), function ($child) {
|
||||
return empty($child['hidden']);
|
||||
});
|
||||
$value = WPStarter_Settings::get($key);
|
||||
$id = 'wps-' . $key;
|
||||
$panel_id = $id . '-panel';
|
||||
$is_bool = 'bool' === $field['type'];
|
||||
?>
|
||||
<section class="wps-card wps-setting<?php echo ($is_bool && $value) ? ' is-on' : ''; ?><?php echo $children ? ' has-panel' : ''; ?>" data-setting-card="<?php echo esc_attr($key); ?>">
|
||||
<?php // Clicking the header (outside the switch) opens the options; only the switch changes the value. ?>
|
||||
<div class="wps-setting__header"<?php echo $children ? ' data-wps-panel-toggle' : ''; ?>>
|
||||
<?php if ($is_bool) : ?>
|
||||
<span class="wps-switch">
|
||||
<input type="checkbox"
|
||||
role="switch"
|
||||
class="wps-switch__input"
|
||||
id="<?php echo esc_attr($id); ?>"
|
||||
data-wps-setting="<?php echo esc_attr($key); ?>"
|
||||
aria-labelledby="<?php echo esc_attr($id); ?>-title"
|
||||
aria-describedby="<?php echo esc_attr($id); ?>-desc"
|
||||
<?php checked((bool) $value); ?> />
|
||||
<span class="wps-switch__track" aria-hidden="true"></span>
|
||||
</span>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="wps-setting__text">
|
||||
<div class="wps-setting__title-row">
|
||||
<span class="wps-setting__title" id="<?php echo esc_attr($id); ?>-title"><?php echo esc_html($field['label']); ?></span>
|
||||
<span class="wps-status" data-wps-status="<?php echo esc_attr($key); ?>" aria-hidden="true"></span>
|
||||
</div>
|
||||
<?php if (!empty($field['description'])) : ?>
|
||||
<p class="wps-setting__desc" id="<?php echo esc_attr($id); ?>-desc"><?php echo esc_html($field['description']); ?></p>
|
||||
<?php endif; ?>
|
||||
<?php self::render_replaces($field); ?>
|
||||
</div>
|
||||
|
||||
<?php if ($children) : ?>
|
||||
<button type="button"
|
||||
class="wps-setting__expand button-link"
|
||||
aria-expanded="false"
|
||||
aria-controls="<?php echo esc_attr($panel_id); ?>">
|
||||
<span class="wps-setting__expand-label"><?php esc_html_e('Options', 'wp-plugin-starter-template'); ?></span>
|
||||
<span class="dashicons dashicons-arrow-down-alt2" aria-hidden="true"></span>
|
||||
<span class="screen-reader-text"><?php echo esc_html(sprintf(/* translators: %s: setting name */ __('for %s', 'wp-plugin-starter-template'), $field['label'])); ?></span>
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<?php if ($children) : ?>
|
||||
<div class="wps-setting__panel" id="<?php echo esc_attr($panel_id); ?>" hidden>
|
||||
<?php
|
||||
/**
|
||||
* Fires at the top of a setting's options panel, for status
|
||||
* such as progress (wrap output in .wps-panel-note).
|
||||
*
|
||||
* @param string $key Setting key.
|
||||
* @param array $field Schema entry.
|
||||
*/
|
||||
do_action('wpstarter_setting_panel', $key, $field);
|
||||
foreach ($children as $child_key => $child) {
|
||||
self::render_field($child_key, $child);
|
||||
}
|
||||
?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</section>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Note which plugins a setting replaces. While one is active the setting
|
||||
* waits (WPStarter_Feature::replaced_active()); say so, with a
|
||||
* deactivate link where the user can deactivate it here.
|
||||
*
|
||||
* @param array $field Schema entry.
|
||||
*/
|
||||
private static function render_replaces(array $field) {
|
||||
if (empty($field['replaces']) || !is_array($field['replaces'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$active = WPStarter_Feature::active_plugins();
|
||||
$site = self::active_plugins_by_slug();
|
||||
$items = array();
|
||||
$links = array();
|
||||
$busy = array();
|
||||
foreach ($field['replaces'] as $slug => $name) {
|
||||
$items[] = esc_html($name);
|
||||
if (!isset($active[$slug])) {
|
||||
continue;
|
||||
}
|
||||
$busy[] = (string) $name;
|
||||
if (isset($site[$slug]) && current_user_can('deactivate_plugin', $site[$slug])) {
|
||||
// Comes back to this tab, where the setting then takes over.
|
||||
$url = WPStarter_Replaced_Plugins::deactivate_url($site[$slug]);
|
||||
/* translators: %s: plugin name */
|
||||
$links[] = sprintf('<a href="%1$s">%2$s</a>', esc_url($url), esc_html(sprintf(__('Deactivate %s', 'wp-plugin-starter-template'), $name)));
|
||||
}
|
||||
}
|
||||
|
||||
printf(
|
||||
'<p class="wps-replaces">%1$s %2$s</p>',
|
||||
esc_html__('Replaces:', 'wp-plugin-starter-template'),
|
||||
implode(', ', $items) // phpcs:ignore WordPress.Security.EscapeOutput -- escaped above.
|
||||
);
|
||||
if ($busy) {
|
||||
printf(
|
||||
'<p class="wps-replaces wps-replaces__active">%1$s %2$s</p>',
|
||||
/* translators: %s: plugin names */
|
||||
esc_html(sprintf(__('%s is still active, so it does this job and this setting waits until it is deactivated.', 'wp-plugin-starter-template'), implode(', ', $busy))),
|
||||
implode(' · ', $links) // phpcs:ignore WordPress.Security.EscapeOutput -- escaped above.
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Plugins active on this site (not network-wide) keyed by directory slug.
|
||||
*
|
||||
* @return array<string,string> slug => plugin file
|
||||
*/
|
||||
private static function active_plugins_by_slug() {
|
||||
static $active = null;
|
||||
if (null === $active) {
|
||||
$active = array();
|
||||
foreach (WPStarter_Feature::stored_active_plugins() as $file) {
|
||||
$slug = dirname((string) $file);
|
||||
if ('.' !== $slug) {
|
||||
$active[$slug] = (string) $file;
|
||||
}
|
||||
}
|
||||
}
|
||||
return $active;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a child field row.
|
||||
*
|
||||
* @param string $key Setting key.
|
||||
* @param array $field Schema entry.
|
||||
*/
|
||||
public static function render_field($key, array $field) {
|
||||
$value = WPStarter_Settings::get($key);
|
||||
$id = 'wps-' . $key;
|
||||
$desc_id = $id . '-desc';
|
||||
$attrs = sprintf('id="%1$s" data-wps-setting="%2$s" aria-describedby="%3$s"', esc_attr($id), esc_attr($key), esc_attr($desc_id));
|
||||
$is_multi = 'multi' === $field['type'];
|
||||
?>
|
||||
<div class="wps-field">
|
||||
<?php if ($is_multi) : ?>
|
||||
<span class="wps-field__label" id="<?php echo esc_attr($id); ?>-label"><?php echo esc_html($field['label']); ?></span>
|
||||
<?php else : ?>
|
||||
<label class="wps-field__label" for="<?php echo esc_attr($id); ?>"><?php echo esc_html($field['label']); ?></label>
|
||||
<?php endif; ?>
|
||||
<div class="wps-field__control">
|
||||
<?php
|
||||
switch ($field['type']) {
|
||||
case 'select':
|
||||
printf('<select %s>', $attrs); // phpcs:ignore WordPress.Security.EscapeOutput -- escaped above.
|
||||
$choices = WPStarter_Settings::options_for($field);
|
||||
if (!empty($field['open']) && '' !== (string) $value && !array_key_exists((string) $value, $choices)) {
|
||||
// A saved choice whose plugin is not loaded here stays shown.
|
||||
$choices[(string) $value] = (string) $value;
|
||||
}
|
||||
foreach ($choices as $option_value => $option_label) {
|
||||
printf(
|
||||
'<option value="%1$s"%2$s>%3$s</option>',
|
||||
esc_attr((string) $option_value),
|
||||
selected((string) $value, (string) $option_value, false),
|
||||
esc_html($option_label)
|
||||
);
|
||||
}
|
||||
echo '</select>';
|
||||
break;
|
||||
|
||||
case 'multi':
|
||||
$chosen = array_map('strval', (array) $value);
|
||||
$choices = WPStarter_Settings::options_for($field);
|
||||
// With some plugins skipped on this request, a saved
|
||||
// choice may belong to one of them; keep it too.
|
||||
if (!empty($field['open']) || WPStarter_Feature::plugins_skipped()) {
|
||||
// Saved items that are not registered right now stay visible so they can be unticked.
|
||||
foreach (array_diff($chosen, array_map('strval', array_keys($choices))) as $missing) {
|
||||
$choices[$missing] = $missing;
|
||||
}
|
||||
}
|
||||
$long = count($choices) > 12;
|
||||
if ($long) {
|
||||
// Long lists (such as every active plugin) scroll, with quick choices.
|
||||
printf(
|
||||
'<span class="wps-checkboxes__all"><button type="button" class="button-link" data-wps-check-all="%1$s">%2$s</button> · <button type="button" class="button-link" data-wps-check-none="%1$s">%3$s</button></span>',
|
||||
esc_attr($id),
|
||||
esc_html__('Select all', 'wp-plugin-starter-template'),
|
||||
esc_html__('Clear', 'wp-plugin-starter-template')
|
||||
);
|
||||
}
|
||||
// The group carries data-wps-setting; the JS saves every checked value.
|
||||
printf(
|
||||
'<fieldset class="wps-checkboxes%3$s" data-wps-multi %1$s aria-labelledby="%2$s">',
|
||||
$attrs, // phpcs:ignore WordPress.Security.EscapeOutput -- escaped above.
|
||||
esc_attr($id . '-label'),
|
||||
$long ? ' is-long' : ''
|
||||
);
|
||||
foreach ($choices as $option_value => $option_label) {
|
||||
printf(
|
||||
'<label class="wps-checkbox"><input type="checkbox" value="%1$s"%2$s /> %3$s</label>',
|
||||
esc_attr((string) $option_value),
|
||||
checked(in_array((string) $option_value, $chosen, true), true, false),
|
||||
esc_html($option_label)
|
||||
);
|
||||
}
|
||||
echo '</fieldset>';
|
||||
break;
|
||||
|
||||
case 'url':
|
||||
printf(
|
||||
'<input type="text" class="regular-text code" inputmode="url" %1$s value="%2$s" placeholder="%3$s" />',
|
||||
$attrs, // phpcs:ignore WordPress.Security.EscapeOutput -- escaped above.
|
||||
esc_attr((string) $value),
|
||||
esc_attr(isset($field['placeholder']) ? $field['placeholder'] : '')
|
||||
);
|
||||
break;
|
||||
|
||||
case 'times':
|
||||
printf(
|
||||
'<input type="text" class="regular-text" %1$s value="%2$s" placeholder="%3$s" autocomplete="off" />',
|
||||
$attrs, // phpcs:ignore WordPress.Security.EscapeOutput -- escaped above.
|
||||
esc_attr((string) $value),
|
||||
esc_attr(isset($field['placeholder']) ? $field['placeholder'] : '')
|
||||
);
|
||||
break;
|
||||
|
||||
case 'int':
|
||||
printf(
|
||||
'<input type="number" class="small-text" %1$s value="%2$s" min="%3$s" max="%4$s" step="1" inputmode="numeric" />',
|
||||
$attrs, // phpcs:ignore WordPress.Security.EscapeOutput -- escaped above.
|
||||
esc_attr((string) $value),
|
||||
esc_attr(isset($field['min']) ? (string) $field['min'] : ''),
|
||||
esc_attr(isset($field['max']) ? (string) $field['max'] : '')
|
||||
);
|
||||
if (!empty($field['unit'])) {
|
||||
echo ' <span class="wps-field__unit">' . esc_html($field['unit']) . '</span>';
|
||||
}
|
||||
break;
|
||||
|
||||
case 'lines':
|
||||
case 'domains':
|
||||
printf(
|
||||
'<textarea class="large-text code" rows="%1$d" %2$s placeholder="%3$s">%4$s</textarea>',
|
||||
isset($field['rows']) ? (int) $field['rows'] : 3,
|
||||
$attrs, // phpcs:ignore WordPress.Security.EscapeOutput -- escaped above.
|
||||
esc_attr(isset($field['placeholder']) ? $field['placeholder'] : ''),
|
||||
esc_textarea((string) $value)
|
||||
);
|
||||
break;
|
||||
|
||||
case 'media':
|
||||
// The hidden input carries data-wps-setting; the JS sets it
|
||||
// from the media dialog and saves it.
|
||||
$image_id = (int) $value;
|
||||
$preview = $image_id ? wp_get_attachment_image_url($image_id, 'thumbnail') : '';
|
||||
echo '<div class="wps-media" data-wps-media>';
|
||||
printf(
|
||||
'<input type="hidden" data-wps-setting="%1$s" value="%2$s" />',
|
||||
esc_attr($key),
|
||||
esc_attr((string) $image_id)
|
||||
);
|
||||
if ($preview) {
|
||||
printf('<img class="wps-media__preview" src="%s" alt="" />', esc_url($preview));
|
||||
} else {
|
||||
echo '<img class="wps-media__preview" alt="" hidden />';
|
||||
}
|
||||
printf(
|
||||
'<button type="button" class="button wps-media__choose" id="%1$s" aria-describedby="%2$s">%3$s</button>',
|
||||
esc_attr($id),
|
||||
esc_attr($desc_id),
|
||||
esc_html__('Choose picture', 'wp-plugin-starter-template')
|
||||
);
|
||||
printf(
|
||||
'<button type="button" class="button-link wps-media__remove"%1$s>%2$s</button>',
|
||||
$image_id ? '' : ' hidden',
|
||||
esc_html__('Remove', 'wp-plugin-starter-template')
|
||||
);
|
||||
echo '</div>';
|
||||
break;
|
||||
|
||||
case 'bool':
|
||||
printf(
|
||||
'<span class="wps-switch"><input type="checkbox" role="switch" class="wps-switch__input" %1$s %2$s /><span class="wps-switch__track" aria-hidden="true"></span></span>',
|
||||
$attrs, // phpcs:ignore WordPress.Security.EscapeOutput -- escaped above.
|
||||
checked((bool) $value, true, false)
|
||||
);
|
||||
break;
|
||||
|
||||
case 'text':
|
||||
default:
|
||||
printf(
|
||||
'<input type="text" class="regular-text" %1$s value="%2$s" placeholder="%3$s" />',
|
||||
$attrs, // phpcs:ignore WordPress.Security.EscapeOutput -- escaped above.
|
||||
esc_attr((string) $value),
|
||||
esc_attr(isset($field['placeholder']) ? $field['placeholder'] : '')
|
||||
);
|
||||
break;
|
||||
}
|
||||
?>
|
||||
<span class="wps-status" data-wps-status="<?php echo esc_attr($key); ?>" aria-hidden="true"></span>
|
||||
<?php if (!empty($field['description']) || !empty($field['tokens'])) : ?>
|
||||
<p class="description" id="<?php echo esc_attr($desc_id); ?>">
|
||||
<?php echo esc_html(isset($field['description']) ? $field['description'] : ''); ?>
|
||||
<?php if (!empty($field['tokens'])) : ?>
|
||||
<span class="wps-tokens">
|
||||
<?php esc_html_e('Tokens:', 'wp-plugin-starter-template'); ?>
|
||||
<?php foreach ($field['tokens'] as $token) : ?>
|
||||
<button type="button" class="wps-token" data-token="<?php echo esc_attr($token); ?>" data-target="<?php echo esc_attr($id); ?>" title="<?php esc_attr_e('Insert token', 'wp-plugin-starter-template'); ?>"><code><?php echo esc_html($token); ?></code></button>
|
||||
<?php endforeach; ?>
|
||||
</span>
|
||||
<?php endif; ?>
|
||||
</p>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,322 @@
|
||||
/**
|
||||
* WP Plugin Starter admin screen.
|
||||
*
|
||||
* One delegated controller per concern:
|
||||
* - Settings: instant save for [data-wps-setting] controls.
|
||||
* - Panels: expandable setting options.
|
||||
* - Tokens: insert pattern tokens into text fields.
|
||||
* - Media fields: choose a Media Library picture with the media dialog.
|
||||
*
|
||||
* The plugin's own tabs bring their own script (wpstarter_admin_enqueue),
|
||||
* which can use window.wpstarterAdmin.api.
|
||||
*
|
||||
* Localized data: window.wpstarterAdmin (see WPStarter_Admin_Manager::enqueue_assets()).
|
||||
*/
|
||||
(function ($, wp, cfg) {
|
||||
'use strict';
|
||||
|
||||
if (!cfg) {
|
||||
return;
|
||||
}
|
||||
|
||||
var i18n = cfg.i18n || {};
|
||||
|
||||
function speak(message, politeness) {
|
||||
if (wp && wp.a11y && wp.a11y.speak) {
|
||||
wp.a11y.speak(message, politeness || 'polite');
|
||||
}
|
||||
}
|
||||
|
||||
// Some plugins redirect the first admin request after activation to a
|
||||
// welcome screen, and admin-ajax.php runs admin_init too. The redirect
|
||||
// happens once, so a reply that is a page instead of JSON is retried
|
||||
// once. Every action here is safe to repeat.
|
||||
function post(action, data) {
|
||||
var deferred = $.Deferred();
|
||||
var current;
|
||||
var send = function (retry) {
|
||||
current = $.ajax({
|
||||
url: cfg.ajaxUrl,
|
||||
type: 'POST',
|
||||
dataType: 'json',
|
||||
data: $.extend({ action: action, nonce: cfg.nonce }, data)
|
||||
})
|
||||
.done(deferred.resolve)
|
||||
.fail(function (xhr, status, error) {
|
||||
if (status === 'parsererror' && retry) {
|
||||
send(false);
|
||||
} else {
|
||||
deferred.reject(xhr, status, error);
|
||||
}
|
||||
});
|
||||
};
|
||||
send(true);
|
||||
|
||||
var promise = deferred.promise();
|
||||
promise.abort = function () {
|
||||
current.abort();
|
||||
};
|
||||
return promise;
|
||||
}
|
||||
|
||||
function errorMessage(xhr, fallback) {
|
||||
var json = xhr && xhr.responseJSON;
|
||||
if (json && json.data && json.data.message) {
|
||||
return json.data.message;
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Settings */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
var Settings = {
|
||||
pending: {},
|
||||
sequence: 0,
|
||||
// Saves run one at a time so each reads the settings the last one wrote.
|
||||
queue: $.Deferred().resolve().promise(),
|
||||
|
||||
init: function () {
|
||||
$(document).on('change', '[data-wps-setting]', function () {
|
||||
Settings.save($(this));
|
||||
});
|
||||
// "Select all" and "Clear" for long checkbox lists: one save for the lot.
|
||||
$(document).on('click', '[data-wps-check-all], [data-wps-check-none]', function () {
|
||||
var all = this.hasAttribute('data-wps-check-all');
|
||||
var $group = $('#' + $(this).attr(all ? 'data-wps-check-all' : 'data-wps-check-none'));
|
||||
$group.find(':checkbox').prop('checked', all);
|
||||
Settings.save($group);
|
||||
});
|
||||
// Forms that change data elsewhere ask first.
|
||||
$(document).on('submit', 'form[data-wps-confirm]', function (event) {
|
||||
if (!window.confirm($(this).attr('data-wps-confirm'))) {
|
||||
event.preventDefault();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
valueOf: function ($input) {
|
||||
if ($input.is('[data-wps-multi]')) {
|
||||
return $input.find(':checkbox:checked').map(function () {
|
||||
return this.value;
|
||||
}).get();
|
||||
}
|
||||
if ($input.is(':checkbox')) {
|
||||
return $input.is(':checked') ? '1' : '0';
|
||||
}
|
||||
return $input.val();
|
||||
},
|
||||
|
||||
status: function (key, state, text, keep) {
|
||||
var $status = $('[data-wps-status="' + key + '"]');
|
||||
$status.removeClass('is-saving is-saved is-error').addClass(state ? 'is-' + state : '').text(text || '');
|
||||
clearTimeout($status.data('timer'));
|
||||
// "Reload the page" stays until the next change.
|
||||
if (state === 'saved' && !keep) {
|
||||
$status.data('timer', setTimeout(function () {
|
||||
$status.removeClass('is-saved').text('');
|
||||
}, 2000));
|
||||
}
|
||||
},
|
||||
|
||||
save: function ($input) {
|
||||
var key = $input.data('wps-setting');
|
||||
var value = this.valueOf($input);
|
||||
var previous = $input.data('wps-saved');
|
||||
var seq = ++this.sequence;
|
||||
|
||||
if (previous === undefined) {
|
||||
previous = $input.is(':checkbox') ? ($input.is(':checked') ? '0' : '1') : value;
|
||||
}
|
||||
|
||||
this.pending[key] = seq;
|
||||
this.status(key, 'saving', i18n.saving);
|
||||
|
||||
var run = function () {
|
||||
return post('wpstarter_save_setting', { key: key, value: value })
|
||||
.done(done)
|
||||
.fail(failed);
|
||||
};
|
||||
var done = function (response) {
|
||||
if (Settings.pending[key] !== seq) {
|
||||
return; // A newer save for this key is queued.
|
||||
}
|
||||
if (!response || !response.success) {
|
||||
Settings.fail($input, key, previous, i18n.saveFailed);
|
||||
return;
|
||||
}
|
||||
var saved = response.data.value;
|
||||
if ($input.is('[data-wps-multi]')) {
|
||||
var chosen = $.map(saved || [], String);
|
||||
$input.find(':checkbox').each(function () {
|
||||
this.checked = $.inArray(this.value, chosen) !== -1;
|
||||
});
|
||||
} else if ($input.is(':checkbox')) {
|
||||
$input.data('wps-saved', saved ? '1' : '0');
|
||||
} else if (saved !== undefined && String(saved) !== String($input.val())) {
|
||||
$input.val(saved); // Show the sanitized value.
|
||||
}
|
||||
if (!$input.is(':checkbox')) {
|
||||
$input.data('wps-saved', $input.val());
|
||||
}
|
||||
var reload = !!response.data.reload;
|
||||
var message = reload && response.data.message ? response.data.message : i18n.saved;
|
||||
Settings.status(key, 'saved', message, reload);
|
||||
speak(message);
|
||||
$(document).trigger('wpstarter:setting-saved', [key, saved]);
|
||||
};
|
||||
var failed = function (xhr) {
|
||||
if (Settings.pending[key] === seq) {
|
||||
Settings.fail($input, key, previous, errorMessage(xhr, i18n.saveFailed));
|
||||
}
|
||||
};
|
||||
|
||||
// A failed save must not block the ones after it.
|
||||
this.queue = this.queue.then(run, run);
|
||||
|
||||
if ($input.is(':checkbox')) {
|
||||
$input.closest('[data-setting-card]').filter(function () {
|
||||
return $(this).data('setting-card') === key;
|
||||
}).toggleClass('is-on', $input.is(':checked'));
|
||||
}
|
||||
},
|
||||
|
||||
fail: function ($input, key, previous, message) {
|
||||
if ($input.is(':checkbox')) {
|
||||
var on = previous === '1' || previous === true;
|
||||
$input.prop('checked', on);
|
||||
$input.closest('[data-setting-card]').toggleClass('is-on', on);
|
||||
}
|
||||
this.status(key, 'error', message);
|
||||
speak(message, 'assertive');
|
||||
}
|
||||
};
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Expandable panels and tokens */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
var Panels = {
|
||||
toggle: function ($button) {
|
||||
if (!$button.length) {
|
||||
return;
|
||||
}
|
||||
var expanded = $button.attr('aria-expanded') === 'true';
|
||||
var panel = document.getElementById($button.attr('aria-controls'));
|
||||
$button.attr('aria-expanded', expanded ? 'false' : 'true');
|
||||
if (panel) {
|
||||
panel.hidden = expanded;
|
||||
}
|
||||
$button.closest('.wps-setting').toggleClass('is-expanded', !expanded);
|
||||
},
|
||||
|
||||
init: function () {
|
||||
$(document).on('click', '.wps-setting__expand', function (event) {
|
||||
event.stopPropagation();
|
||||
Panels.toggle($(this));
|
||||
});
|
||||
|
||||
// Mouse convenience: clicking anywhere on the header opens/closes the
|
||||
// options. The switch (and any other control) keeps its own behaviour;
|
||||
// keyboard users use the Options button.
|
||||
$(document).on('click', '[data-wps-panel-toggle]', function (event) {
|
||||
if ($(event.target).closest('input, button, a, label, select, textarea, .wps-switch').length) {
|
||||
return;
|
||||
}
|
||||
if (window.getSelection && String(window.getSelection()).length) {
|
||||
return; // Let people select text.
|
||||
}
|
||||
Panels.toggle($(this).find('.wps-setting__expand').first());
|
||||
});
|
||||
|
||||
$(document).on('click', '.wps-token', function () {
|
||||
var input = document.getElementById($(this).data('target'));
|
||||
var token = String($(this).data('token'));
|
||||
if (!input) {
|
||||
return;
|
||||
}
|
||||
var start = typeof input.selectionStart === 'number' ? input.selectionStart : input.value.length;
|
||||
var end = typeof input.selectionEnd === 'number' ? input.selectionEnd : input.value.length;
|
||||
input.value = input.value.slice(0, start) + token + input.value.slice(end);
|
||||
input.focus();
|
||||
input.setSelectionRange(start + token.length, start + token.length);
|
||||
$(input).trigger('change');
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Media fields: choose a Media Library picture */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
var MediaField = {
|
||||
init: function () {
|
||||
$(document).on('click', '.wps-media__choose', function () {
|
||||
MediaField.open($(this).closest('[data-wps-media]'));
|
||||
});
|
||||
$(document).on('click', '.wps-media__remove', function () {
|
||||
MediaField.set($(this).closest('[data-wps-media]'), 0, '');
|
||||
$(this).siblings('.wps-media__choose').trigger('focus');
|
||||
});
|
||||
// A saved 0 means the choice was not a picture.
|
||||
$(document).on('wpstarter:setting-saved', function (event, key, value) {
|
||||
var $field = $('[data-wps-media]').has('[data-wps-setting="' + key + '"]');
|
||||
if ($field.length && !parseInt(value, 10)) {
|
||||
MediaField.preview($field, '');
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
open: function ($field) {
|
||||
if (!wp || !wp.media) {
|
||||
return;
|
||||
}
|
||||
var frame = $field.data('wps-frame');
|
||||
if (!frame) {
|
||||
frame = wp.media({
|
||||
title: i18n.chooseImage,
|
||||
library: { type: 'image' },
|
||||
multiple: false,
|
||||
button: { text: i18n.useImage }
|
||||
});
|
||||
frame.on('select', function () {
|
||||
var item = frame.state().get('selection').first();
|
||||
if (!item) {
|
||||
return;
|
||||
}
|
||||
var data = item.toJSON();
|
||||
var url = data.sizes && data.sizes.thumbnail ? data.sizes.thumbnail.url : data.url;
|
||||
MediaField.set($field, data.id, url);
|
||||
});
|
||||
$field.data('wps-frame', frame);
|
||||
}
|
||||
frame.open();
|
||||
},
|
||||
|
||||
set: function ($field, id, url) {
|
||||
this.preview($field, url);
|
||||
$field.find('[data-wps-setting]').val(String(id || 0)).trigger('change');
|
||||
},
|
||||
|
||||
preview: function ($field, url) {
|
||||
var $img = $field.find('.wps-media__preview');
|
||||
if (url) {
|
||||
$img.attr('src', url).prop('hidden', false);
|
||||
} else {
|
||||
$img.removeAttr('src').prop('hidden', true);
|
||||
}
|
||||
$field.find('.wps-media__remove').prop('hidden', !url);
|
||||
}
|
||||
};
|
||||
|
||||
// For the plugin's own admin scripts, which load after this one
|
||||
// (wpstarter_admin_enqueue): the same AJAX, screen reader and error helpers.
|
||||
cfg.api = { post: post, speak: speak, errorMessage: errorMessage };
|
||||
|
||||
$(function () {
|
||||
Settings.init();
|
||||
Panels.init();
|
||||
MediaField.init();
|
||||
});
|
||||
})(jQuery, window.wp, window.wpstarterAdmin);
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
/**
|
||||
* WP Plugin Starter admin loader.
|
||||
*
|
||||
* Loads the settings screen, the Read Me tab and the Plugins screen notes
|
||||
* for replaced plugins, then the plugin's own admin parts
|
||||
* (WPStarter_Setup::admin()). Included from the main plugin file for
|
||||
* admin requests only.
|
||||
*
|
||||
* @package WPStarter
|
||||
*/
|
||||
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
$wpstarter_admin_files = array(
|
||||
'admin/data/readme.php',
|
||||
'admin/includes/class-settings-manager.php',
|
||||
'admin/includes/class-readme-manager.php',
|
||||
'admin/includes/class-admin-manager.php',
|
||||
'admin/includes/class-replaced-plugins.php',
|
||||
);
|
||||
|
||||
foreach ($wpstarter_admin_files as $wpstarter_file) {
|
||||
require_once WPSTARTER_DIR . $wpstarter_file;
|
||||
}
|
||||
unset($wpstarter_admin_files, $wpstarter_file);
|
||||
|
||||
WPStarter_Admin_Manager::init();
|
||||
WPStarter_Replaced_Plugins::init();
|
||||
WPStarter_Setup::admin();
|
||||
Reference in new issue
Block a user