Theatrum Admin
WordPress admin-customization plugin — reshaped sidebar, pattern and template management screens, cross-post-type tag view, and a screen-reader-only block toggle. PHP + Vite/TypeScript.
by Anna Jennings (Theatrum Mundi) · github.com/abananaj/theatrum-admin · website
Install
No release zip yet. The repository archive installs, but the folder name will carry the branch suffix and updates will not flow:
wp plugin install https://github.com/abananaj/theatrum-admin/archive/refs/heads/main.zipWordPress admin-customization plugin for Chance Theater. Reshapes the wp-admin sidebar, adds pattern/template management screens, a cross-post-type tag view, and a Screen-Reader-Only block toggle. Mostly server-side PHP, with a small Vite/TypeScript editor layer.
📸 Snapshot: This README documents the plugin as of 2026-06-30, captured prior to a round of large changes. It reflects current code, not historical intent.
Quick facts
| Type | Site-specific must-have plugin (git submodule) |
| Stack | PHP · Vite 8 · TypeScript · SCSS · @wordpress/* editor packages |
| Loads | submenus · media-library-assistant · patterns-admin · design-system · sr-only-blocks · position-controls |
| Version | 1.0.0 (plugin header + package.json agree ✅) |
| Text Domain | chance-theater (declared; no /languages loaded) |
| PHP | 8.0+ required (not "recommended" — str_ends_with() fatals on 7.4; see jul5-code-review.md #6) |
🔗 Parent project CLAUDE.md · Theme README · SR-Only feature doc
Architecture
A thin loader (theatrum-admin.php) defines THEATRUM_ADMIN_DIR and require_onces the active modules in inc/, then enqueues the SR-Only editor bundle. Everything is hook-driven; no classes.
Directory map
theatrum-admin/
├── theatrum-admin.php # loader + all editor-script/style enqueues
├── inc/
│ ├── submenus.php # ✅ admin menu reorder, Tags view, tagged-posts page
│ ├── media-library-assistant.php # media library admin customizations
│ ├── patterns-admin.php # ✅ wp_block tag support, usage count, list columns
│ ├── design-system.php # ✅ Templates / Patterns / Parts admin pages
│ ├── sr-only-blocks.php # ✅ srOnly attribute + render_block class injection
│ └── position-controls.php # ✅ positionType/Top/Right/Bottom/Left attrs + render_block style injection
├── src/
│ ├── index.ts # bundles SCSS
│ ├── sr-only-blocks.tsx # editor toggle + outline badge (editor build)
│ ├── position-controls.tsx # Position panel (Static/Relative/Absolute/Fixed/Sticky + UnitControl offsets)
│ ├── custom-formats.tsx # RichText formats: Inline Quote, Small Text, Span
│ ├── hgroup-control.tsx # Group block <hgroup> toggle
│ └── scss/ # sr-only.scss, svg-media-library.scss
├── dist/ # build output (gitignored) — rebuild in every environment
├── vite.config.js # builds src/index.ts → dist/index.js (IIFE, self-injects CSS)
├── vite.config.editor.js # builds src/sr-only-blocks.tsx → dist/sr-only-blocks.js
├── vite.config.position.js # builds src/position-controls.tsx → dist/position-controls.js
├── vite.config.formats.js # builds src/custom-formats.tsx → dist/custom-formats.js
└── vite.config.hgroup.js # builds src/hgroup-control.tsx → dist/hgroup-control.js
⚠️ The build/enqueue wiring is out of sync — see Next Steps #1. The dependable, working surface of this plugin is the PHP in
submenus.php,patterns-admin.php, anddesign-system.php.
What it does
🗂️ Admin menu reshaping — submenus.php
- Renames Posts → Blog; reorders top-level menu to Media · Pages · Blog.
- Promotes Comments and Tags to deliberate positions. ⚠️ "Themes moved from Appearance into Settings" does not work — implemented twice (
submenus.phpanddesign-system.php), both use the wrong menu-parent key. Themes stays under Appearance. See Next Steps 🔴. - Inserts visual separators into Blog and CPT submenus.
- Productions: renames "All Productions" → Chance Productions, orders Series before Season, adds a Visiting Companies submenu, and filters the main list to exclude the
visiting-companiesseries. - Hides the Tags submenu on CPTs and adds a global Tagged Posts screen that lists every public post type sharing a tag (the tag-count column links to it).
🧩 Pattern management — patterns-admin.php + design-system.php
- Registers
post_tagonwp_blockso synced patterns can be tagged. ct_count_pattern_usage()— accurate "Used In" count via a preparedLIKEon the"ref":IDblock comment, with a hidden detail page listing every location.- Adds Category / Tags / Used In columns to the native
wp_blocklist and makes it filterable by?wp_pattern_category=. - Appearance pages for Templates, Parts, and a read-only Grouped Pattern Overview (synced/unsynced → category), each merging theme files with DB records.
♿ Screen-Reader-Only blocks — sr-only-blocks.php + sr-only-blocks.tsx
- Adds a "Screen Reader Only" toggle (Accessibility panel) to
core/headingandcore/paragraph, plus an SR-ONLY badge on the block outline. - Registers the
srOnlyattribute viaregister_block_type_argsand injects the.sr-onlyclass atrender_block. Full notes: SR-ONLY-FEATURE.md.
📐 Position controls — position-controls.php + position-controls.tsx
- Adds a "Position" panel to
chance/cover-cardandchance/chance-cardwith a Static / Relative / Absolute / Fixed / Sticky select, plus fourUnitControloffset inputs (Top, Right, Bottom, Left) laid out in a 2×2 grid once a non-static type is chosen. - Replaces core's native
supports.position(which only offered Sticky/Fixed and a single Top offset — removed from both blocks'block.json). RegisterspositionType/positionTop/positionRight/positionBottom/positionLeftattributes viaregister_block_type_args, previews live in the editor canvas via aneditor.BlockListBlockwrapper-style filter, and injects the computedposition/offset CSS into the wrapper'sstyleattribute atrender_block(offset values are validated against a CSS-length pattern before being written out).
CPTs this plugin touches (defined in the theme): 🎬 production · 🎟️ event · 👤 artist · 🎓 class · 🏛️ venue · 💛 supporter · 📄 page.
Build & Development
npm install
npm run build # vite build (index.js) + vite.config.editor.js (sr-only-blocks.js) + vite.config.position.js (position-controls.js)
npm run build:watch # watch index build only
npm run deploy # same as build
dist/ is gitignored — assets must be rebuilt in each environment. npm run build runs all three configs; a partial build (only the first) leaves the SR-Only and/or Position features without their scripts. See Next Steps #1.
Code Review — Current State
Reviewed broadly for correctness, WP standards, security, performance, and architecture. The PHP layer is in good shape:
✅ Pattern usage count is $wpdb->prepare'd with esc_like, and matches "ref":ID} / "ref":ID, to avoid false positives (5 vs 50) · ✅ consistent output escaping (esc_url / esc_html / (int)) across all admin tables · ✅ $_GET inputs sanitized (sanitize_text_field, (int)) · ✅ ABSPATH guards · ✅ admin screens capability-gated via add_submenu_page · ✅ native wp_block list reused (keeps Quick/Bulk Edit) instead of a custom table.
The remaining issues are concentrated in the JS/build layer, which has drifted from the PHP that loads it.
Next Steps — by severity
Most of these are intended to be fixed before archiving.
🔴 High
dist/is gitignored — must be rebuilt in every environment, andnpm run build:watchonly rebuilds the default config withemptyOutDir: true, so a watch session silently empties the other 4 built files. Run the fullnpm run build(all 5 configs) before relying on any editor feature. (Note: srOnly content that's already set still hides correctly on the frontend — the theme defines.sr-onlyindist/main.css, so a stale build is an authoring breakage, not an accessibility regression.)- Build output ↔ enqueue mismatch across the JS/CSS layer. Two loose wires:
theatrum-admin.phpenqueuesdist/sr-only-blocks.css, but no build step emits it — the SCSS is self-injected byindex.jsinstead. Only impact today is the editor-only SR-ONLY badge cosmetics (the frontend.sr-onlyrule comes from the theme). The editor script also loads in the outer admin frame while the badge renders inside the editor canvas iframe, so even a real CSS file wouldn't reach it without also fixing the iframe-isolation issue.dist/index.jsis built but never enqueued by any PHP. → Decide one strategy: emit named files per feature, or enqueueindex.js+ its CSS. Then make the PHP paths match the build outputs.
- "Themes moved into Settings" is broken, implemented twice.
submenus.phpanddesign-system.phpeach try to move the Themes menu item using the wrong parent-slug key ('themes'/'options'instead ofthemes.php/options-general.php). Delete one implementation and fix the keys in the survivor — seejul5-code-review.md#1 for exact line numbers. - Deploy docs contradict this plugin's
.gitignore. RootCLAUDE.mdsays to stagedist/+build/alongside code, but this plugin ignoresdist/entirely and the deploy flow (git push → SSH pull) has no remote build step — so this plugin's built assets never reach the server as documented. This needs an actual decision (start committingdist/, or add a remote build step) before either doc can be called accurate. Seejul5-code-review.md#8.
🟠 Medium
- Fragile hardcoded menu positions.
submenus.phpswaps menu slots by literal index (5/10/20/30/31/32/58). Another plugin adding menu items can collide. → Look items up by slug instead of assuming positions.
🟡 Low
date()instead ofwp_date()/gmdate()for the "Created" column indesign-system.php— timezone-naive. → Usewp_date().- i18n incomplete. Text Domain
chance-theateris declared and strings use__(), but there's noload_plugin_textdomain()and no/languages. → Wire it up or drop the domain noise. - Anonymous closures on
admin_menu/pre_get_posts/admin_headcan't be unhooked or unit-tested. → Prefer named functions if this grows. - **Inline `