Gopherium's admin kit
GodMin is the host layer for a React admin application built on the WordPress
Design System. Five layers make up such an application, and four already have
owners upstream: @wordpress/theme owns tokens, @wordpress/ui owns
primitives, @wordpress/dataviews owns data screens, and @wordpress/admin-ui
owns page chrome. The fifth is the host layer, which WordPress supplies to
itself through PHP and @wordpress/boot, and which nothing supplies to an
application that is not WordPress. GodMin is that layer.
Every export earns its place against one of three tests: it closes a documented upstream gap, it satisfies a documented host requirement, or it owns a contract that two published packages each expect the application to hand-write. Nothing here duplicates something upstream ships.
| Entry point | Contents |
|---|---|
@gopherium/godmin |
AdminRoot, useTokenDocument, SUPPORTED_WPDS |
@gopherium/godmin/base.css |
cascade layer order, design tokens, host rules |
@gopherium/godmin/testing |
installTestEnvironment, renderAdmin, getAnnouncement, clearAnnouncements, assertElementPatched, WPDS_IGNORE_SELECTOR |
@gopherium/godmin/vite |
godminDedupe, godminSingleCopy |
@gopherium/godmin/stylelint |
the design system stylelint rules |
@gopherium/godmin/patches |
the React 19 patch for @wordpress/element, temporary |
Version 0.1.0 is the host layer. The application frame is deliberately not here
yet, because @wordpress/admin-ui is actively building that layer and a kit
should not race upstream. See the CHANGELOG for what each release adds.
pnpm add @gopherium/godminGodMin declares the design system as peer dependencies, so your application
resolves and pins them. The window this build was tested against is exported as
SUPPORTED_WPDS and enforced by the declared peer ranges.
Import the stylesheet once, at your application entry point, then mount
AdminRoot around your tree.
import '@gopherium/godmin/base.css'
import { AdminRoot } from '@gopherium/godmin'
createRoot(document.getElementById('app')!).render(
<AdminRoot>
<YourApp />
</AdminRoot>,
)That is the whole host setup. base.css loads the design tokens for you, so
you do not import @wordpress/theme/design-tokens.css separately, and it
declares the cascade layer order as wp-ui, godmin. AdminRoot isolates the
stacking context that portaled popovers need and enables the overlay slot that
lets design system overlays stack above @wordpress/components ones.
base.css sets the page font, colour and background from design tokens inside
@layer godmin, so any unlayered rule of your own wins without a specificity
fight. One rule it deliberately leaves unlayered is body { position: relative },
because overlays position against the nearest positioned ancestor and an
application that overrode it would get broken backdrops.
If you declare your own layers, name GodMin's in the order you want:
@layer wp-ui, godmin, my-app;Design system styles are injected per document, so a secondary document such as
an iframe or a popup needs registering. useTokenDocument does that and keeps
it supplied with styles registered later, which is what an embedded editor
canvas needs.
useTokenDocument(iframeRef.current?.contentDocument)@gopherium/godmin/testing solves the problems the design system creates for a
test runner. Call it once from your setup file.
import { installTestEnvironment } from '@gopherium/godmin/testing'
installTestEnvironment()The design system announces through @wordpress/a11y, which appends a live
region and an intro paragraph to the body. Without this, every query for text
that was also announced finds two nodes, and the last announcement of one test
is still in the DOM for the next one. installTestEnvironment makes queries
ignore those elements and empties them after each test.
Because the announcement regions are then invisible to queries, read them directly instead.
import { getAnnouncement } from '@gopherium/godmin/testing'
speak('Draft saved')
expect(getAnnouncement()).toBe('Draft saved')renderAdmin renders a tree inside AdminRoot, so components that read design
tokens behave as they will in the application.
@testing-library/react and vitest are optional peers, needed only for this
entry point.
Several packages break when a bundle contains two copies of them. React is the
loud case, where hooks throw, but a second @wordpress/theme silently gives you
a second context and unthemed components. @gopherium/godmin/vite exports the
list and a plugin that fails the build rather than shipping the bug.
import { godminDedupe, godminSingleCopy } from '@gopherium/godmin/vite'
export default defineConfig({
resolve: { dedupe: godminDedupe },
plugins: [godminSingleCopy()],
})The list cannot live in the package alone, because resolve.dedupe is read
from your own config, so GodMin ships it as data for you to spread.
Up to and including 8.4.0, @wordpress/element imports APIs that React 19
removed, so it fails to load. The fix is a patch, and pnpm applies patches at
install time before node_modules exists, so no published package can supply
one for you. GodMin ships the patch it was tested against for you to copy.
cp node_modules/@gopherium/godmin/patches/*.patch patches/patchedDependencies:
'@wordpress/element@8.4.0': patches/@wordpress__element@8.4.0.patchRather than checking that your patch file matches ours, assert the outcome in your test suite, which is what actually determines whether your application runs.
import { assertElementPatched } from '@gopherium/godmin/testing'
test('element works on React 19', async () => {
await assertElementPatched()
})This is temporary. The fix is merged upstream and lands in a future
@wordpress/element release, after which no patch is needed at all. The
assertion is written for that: it checks that the removed APIs are absent and
the live ones work, so it keeps passing once upstream ships and the patch goes
away, whether that arrives as a minor or a major.
GodMin does not constrain your @wordpress/element version. It never imports
it at runtime, and the version is chosen by @wordpress/ui and
@wordpress/theme, which depend on it directly. The shipped patch file is
removed from this package once the fixed release is the one those packages
resolve.
@wordpress/theme ships three stylelint rules and turning them on requires
knowing they exist and what they are called. @gopherium/godmin/stylelint is
that configuration.
export default { extends: ['@gopherium/godmin/stylelint'] }It catches design tokens that do not exist, an application redefining a
--wpds- property, and hand written fallback values. The first of those is the
one that bites quietly, since a mistyped token renders nothing at all.
stylelint is an optional peer, needed only for this entry point.
Peer ranges are longhand and single window, for example >=0.19.0 <0.20.0. The
window MOVES on each design system train bump and never widens, so no release
accepts two breaking generations at once. @wordpress/ui is pre 1.0, where a
caret range covers only one minor, and reading a caret as broader than that is
what silently breaks consumers.
Apache-2.0, see LICENSE.
The design system packages this kit expects you to install are GPL-2.0-or-later. GodMin declares them as peer dependencies, so your application resolves them and GodMin never redistributes them. Your built bundle combines both, and is conveyed under the terms of the WordPress packages. GodMin's own source stays reusable under plain Apache-2.0. NOTICE carries the same statement for anyone reading only the published tarball.