A small, server-rendered admin-UI layer that every Perxel WordPress plugin bundles verbatim. It provides one master layout (feature sidebar + main) and a handful of components built on top of native wp-admin CSS — not a replacement for it.
In the plugin's main file, after its own constants:
require_once __DIR__ . '/ui/loader.php';
Perxel_UI_Loader::register( '0.3.0', __DIR__ . '/ui', plugins_url( 'ui', __FILE__ ) );In each admin-page callback:
Perxel_UI::enqueue();
Perxel_UI_Layout::open( array(
'title' => __( 'Status', 'my-plugin' ),
'plugin' => 'My Plugin',
'version' => MY_PLUGIN_VERSION,
'menu' => array( '' => array(
'my-plugin' => __( 'Status', 'my-plugin' ),
'my-plugin-settings' => __( 'Settings', 'my-plugin' ),
) ),
'current' => 'my-plugin',
'base' => 'admin.php', // admin base file the sidebar links off
) );
include __DIR__ . '/views/status.php'; // plugin-owned main content
Perxel_UI_Layout::close();Perxel_UI::enqueue() must run on admin_enqueue_scripts for the page.
ui/is versioned independently of the plugin (seeCHANGELOG.md). Bump it when the kit changes, not when the plugin does.- The loader keeps the highest registered copy when several plugins are active; the others are inert. Two versions never collide.
- Within a major version the public API is additive-only. Dropping
ui/from a newer plugin into an older one, or vice versa, can never fatal and never changes plugin behaviour — at worst a plugin that needs a newer kit shows an admin notice (Perxel_UI_Loader::require_version()). - A breaking change = major bump, and every plugin must adopt the new
ui/before shipping it. loader.phpitself must stay backwards compatible forever — it is the one file an old plugin still runs when a newer copy wins.
Perxel_UI_Layout
| Method | Purpose |
|---|---|
open( array $args ) |
.wrap → shell → sidebar (sticky brand bar: plugin) → <main> (sticky title bar: <h1> + actions). Args: title, plugin, version, menu, current, base, links, author, actions, wrap_class, text_domain. actions is trusted HTML pinned to the right of the title bar — the house home for a page's Save button; wire it to the page's <form> with the HTML5 form="<form-id>" attribute (get_submit_button( $text, 'primary', 'submit', false, array( 'form' => 'my-form' ) )). author ([ 'name' => …, 'url' => … ]) and version show left in the footer; links ([ label => url ]) show right. |
close() |
Renders the footer (author + version left, links right) inside <main>, then closes what open() opened. |
set_page_titles( array $map, $plugin = '' ) |
[ page_slug => page name ] + a plugin name. Own the browser <title> for the kit's screens: the tab reads Site • Page • Plugin (via the admin_title filter) instead of WP's "Page ‹ Site — WordPress", or the bare " ‹ Site — WordPress" a remove_submenu_page()d screen is left with. Call on admin_menu with the slugs passed to add_submenu_page(). Additive, idempotent. |
Perxel_UI (each returns an HTML string — echo it)
| Method | Purpose |
|---|---|
enqueue() |
Registers the kit CSS/JS under the shared perxel-ui handle. |
notice( $type, $html, $args ) |
success|warning|error|info, on WP .notice. $args: dismissible, inline (stay put instead of being hoisted to .wp-header-end). |
progress_bar( $pct, $args ) |
Standalone bar. $args: id, label (trusted HTML caption below). |
stat_grid( $tiles ) |
Tile: label, value, sub, bar (0-100|null), tone (good|warn|bad). |
card( $args ) |
title, body, actions, id, class. |
rows( $groups ) |
iOS-style grouped settings list. Flat row list, or groups [ 'title' => …, 'rows' => [ … ] ] — optional title above a rounded card of rows (the card is the only shadowed element). Row: label left, content right (text, toggle(), a <select>, a button), plus sub, tone (good|warn|bad), icon. icon puts a fixed 20px square left of the label + sub (centred against both): good|warn|bad is a filled ✓/!/✕ status dot, any other string is trusted HTML (dashicon, <svg>, emoji) scaled to the frame. A row with a summary key is a disclosure instead: native <details> styled as a row (summary text left, optional content + chevron right), details (trusted HTML) revealing full-width below on click; also takes sub, open, tone, icon. A group with 'danger' => true renders as a destructive zone (red title + hairline card, buttons in the warning colour). A group with a note (trusted HTML or plain string) shows it as a muted footnote below the card — a group description or "learn more" link. |
toggle( $args ) |
A checkbox the kit renders as an iOS switch. name, checked, value, id, form, label. A bare <input type="checkbox"> in .pxui-wrap looks identical. |
checkbox_group( $args ) |
A "pick several" list rendered as selectable pills (real hidden checkboxes underneath). options (value => label, or per-option value/label/sub (muted second line)/checked), name (auto []), form, selected. Drop in as row content. Add .pxui-checkbox to a bare <input type="checkbox"> for a real square box instead of the iOS toggle. |
code( $text, $args ) |
Read-only preformatted block (config, rules, logs) — scrolls sideways, doesn't wrap. $text escaped; $args: label (caption), id. Reads well as a disclosure row's details. |
spinner() |
Inline CSS loading spinner (.pxui-spinner). |
The helpers escape their own structural markup and the title / label fields.
body, actions, value, content, sub are treated as trusted HTML —
the caller escapes their dynamic parts. At the call site:
echo Perxel_UI::rows( $groups ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Perxel_UI escapes internally.- In
ui/: anything another Perxel plugin could plausibly reuse — layout, notices, progress bars, stat tiles, cards, row groups (incl. the danger group), tokens. - In the plugin: anything specific to that plugin's domain (its own widgets,
domain-specific tables). Plugin CSS/JS may be inline or in the plugin's own
assets/. - Grey area: start plugin-local; promote to
ui/when a second plugin needs it, and bump the kit version.
Tools → Perxel UI renders every component in the real layout. Always
registered in the admin (visible to manage_options) — the review surface after
any ui/ change.
A plugin can host the showcase as one of its own screens instead: define
PERXEL_UI_SHOWCASE_HOSTED (truthy, before the kit boots) to suppress the Tools
page, then echo Perxel_UI_Showcase::body() between your own
Perxel_UI_Layout::open() / close().
- Server-rendered PHP + a few lines of vanilla JS. No build step, no framework.
- Two stylesheets, both enqueued by
Perxel_UI::enqueue():assets/ui.css(tokens, layout, surface treatment, display components) andassets/ui-forms.css(row groups, form controls, spinner — it reads the tokensui.cssdeclares, so it loads second and depends onperxel-ui). Each stays around ~500 lines. Past that, you're fighting wp-admin instead of using it. - Kit files are plain (no namespace) and loaded by
loader.php, never by a plugin's autoloader. Prefixes:Perxel_UI,perxel_ui,PERXEL_UI,pxui.