-
Notifications
You must be signed in to change notification settings - Fork 41
example register desktop theme
Status: Experimental
A desktop theme reskins the whole shell. Site admins normally install one by uploading a ZIP in OpenStation Preferences → Themes, but a plugin can ship one directly — same sanitizer, same compiler, same constraints. The only difference is that your assets are absolute URLs you already serve instead of files inside an archive.
Not the same as
openstation_register_window_theme(), which restyles one window's chrome. This restyles the entire OS.
<?php
/**
* Plugin Name: Acme Neon Glass
* Description: A desktop theme for OpenStation.
* Requires Plugins: desktop-mode
*/
defined( 'ABSPATH' ) || exit;
add_action( 'init', function () {
if ( ! function_exists( 'openstation_register_desktop_theme' ) ) {
return;
}
$asset = function ( $file ) {
return plugins_url( 'theme/' . $file, __FILE__ );
};
$result = openstation_register_desktop_theme( 'acme/neon-glass', array(
'name' => __( 'Neon Glass', 'acme-neon-glass' ),
'version' => '1.0.0',
'author' => 'Acme Design',
'description' => __( 'Deep indigo glass with a neon rim.', 'acme-neon-glass' ),
'preview' => $asset( 'preview.png' ),
// Every `--os-*` custom property the shell defines
// is fair game. See assets/css/variables.css for the full set.
'tokens' => array(
'--os-window-bg' => '#12122a',
'--os-window-border' => '#2b2b52',
'--os-titlebar-bg' => '#171733',
'--os-titlebar-bg-focused' => '#241f4d',
'--os-titlebar-color' => '#a8a8c0',
'--os-dock-bg' => 'rgba( 12, 12, 30, 0.72 )',
'--wp-admin-theme-color' => '#7c5cff',
// Typography. `--os-font` styles the chrome,
// `--os-ui-font` the window bodies — the classic desktop
// split. Always end the stack with a generic family.
'--os-font' => '"Neon Grotesk", system-ui, sans-serif',
'--os-ui-font' => '"Neon Grotesk", system-ui, sans-serif',
'--os-ui-font-mono' => '"Neon Mono", ui-monospace, monospace',
),
// One entry per @font-face. PHP generates the at-rule; you
// supply a family name and absolute URLs. Declaring a face
// does not use it — the tokens above are what point at it.
'fonts' => array(
array(
'family' => 'Neon Grotesk',
'weight' => '400',
'style' => 'normal',
'display' => 'swap',
'unicodeRange' => 'U+0000-00FF',
'src' => array(
$asset( 'fonts/neon-grotesk-400.woff2' ),
),
),
array(
'family' => 'Neon Grotesk',
'weight' => '700',
'display' => 'swap',
'src' => array( $asset( 'fonts/neon-grotesk-700.woff2' ) ),
),
),
// Wallpapers a user can pick in OpenStation Preferences → Wallpaper.
// Activating the theme does NOT switch to them — see the
// "It is a pick, not an act" note in the theme docs.
'wallpapers' => array(
'dusk' => array(
'path' => $asset( 'wallpapers/dusk.jpg' ),
'label' => __( 'Dusk', 'acme-neon-glass' ),
),
'dawn' => array(
'path' => $asset( 'wallpapers/dawn.jpg' ),
'label' => __( 'Dawn', 'acme-neon-glass' ),
),
),
'icons' => array(
'WINDOW_CONTROL_CLOSE' => array(
'type' => 'image',
'path' => $asset( 'icons/close.svg' ),
),
'OS_SETTINGS' => array(
'type' => 'image',
'path' => $asset( 'icons/settings.svg' ),
),
// A slot can also just point at a different dashicon.
'APP:edit-php' => array(
'type' => 'dashicon',
'name' => 'dashicons-edit-large',
),
),
'textures' => array(
'TITLEBAR' => array(
'type' => 'image',
'path' => $asset( 'textures/titlebar.png' ),
'repeat' => 'repeat-x',
'size' => 'auto 100%',
),
'WINDOW_FRAME' => array(
'type' => 'border-image',
'path' => $asset( 'textures/frame.png' ),
'slice' => '24 fill',
'width' => '12px',
'repeat' => 'round',
),
// Component-kit slots reach every instance of that
// component anywhere in the OS. Keep them subtle — these
// tile across small surfaces.
'MENU' => array(
'type' => 'image',
'path' => $asset( 'textures/noise.png' ),
),
'BUTTON' => array(
'type' => 'image',
'path' => $asset( 'textures/sheen.png' ),
'size' => '100% 100%',
),
),
// The arrangement this theme was designed against. Seeded into
// a user's own preferences the FIRST time they activate the
// theme, and never again — anything they change afterwards is
// theirs. See "Recommended OS settings" in the theme docs.
'recommendedOsSettings' => array(
'dockSize' => 'large',
'desktopLayout' => 'unified',
),
) );
if ( is_wp_error( $result ) ) {
// Structural problems (bad id, missing name) fail loudly.
// Everything else drops the offending entry and installs the
// rest — see "Fallback semantics" in the theme docs.
error_log( '[acme-neon-glass] ' . $result->get_error_message() );
}
} );Drop your images under theme/ next to the plugin file and you're
done. The theme appears in OpenStation Preferences → Themes for every user on the
site the moment the plugin activates — no reload, because the
serverDesktopThemes payload rides the existing live-refresh channel.
- Live activation and deactivation. Activating your plugin makes the theme appear in every open shell. Deactivating removes it, and any user currently wearing it falls back to the system default without a reload.
-
Sanitization. Your manifest travels the same validator an
uploaded ZIP does. If a value doesn't apply, check it against the
value grammar — the most common
causes are a
url()(PHP generates those for you) or avar(). -
Slug precedence. If a site admin has uploaded a theme with the
same slug, theirs wins. Namespace your id (
acme/neon-glass) and this won't come up.
Plugins that paint their own chrome can follow along:
// Repaint when the user switches themes. Fires only on a real change.
document.addEventListener( 'os-desktop-theme-changed', ( e ) => {
const { themeId, previous } = e.detail;
myToolbar.repaint();
} );
// Ask the active theme for an icon. `null` means "no theme, or this
// slot isn't overridden" — paint your default.
const closeIcon = wp.os.desktopThemes.resolveIcon(
'WINDOW_CONTROL_CLOSE',
);And themes can be extended by other plugins through the icon filter:
wp.hooks.addFilter(
'os.os-theme.icon',
'my-plugin',
( icon, { slot, themeId } ) =>
slot === 'APP:my-plugin' ? myBrandedIconUrl : icon,
);-
Control glyphs are monochrome. Window control icons paint as a
currentColor-tinted CSS mask so they keep the title bar's focused/unfocused tinting. Only the alpha channel of your image is used. Design them as solid silhouettes. -
previewis worth shipping. Without it the theme card in OS Settings falls back to two initials on a grey rectangle. -
Assets must be reachable.
plugins_url()output is validated as anhttp(s)URL whose extension is on the allowlist for that kind — images forpreview/icons/textures, fonts forfonts. Anything else is dropped. The two lists are disjoint, so pointing aTITLEBARtexture at a.woff2fails silently. -
A declared font is not a used font.
fontsdefines faces; the typography tokens are what reference them. Declare both or nothing changes. -
Recommendations fire once per user.
recommendedOsSettingsis seeded on a user's first activation of your theme and never re-asserted. If you're testing it and nothing happens, you have already been seeded — use the Apply <theme>'s recommended layout button in OpenStation Preferences → Themes, orwp.os.desktopThemes.applyRecommendedOsSettings(). - Licensing is yours. A bundled font is redistributed to every visitor of every site that installs the theme. Ship one whose licence permits that.
- Desktop themes — manifest format, slot tables, value grammar
- Hooks reference — the PHP filters and actions
-
JavaScript reference —
wp.os.desktopThemes - Window themes — the per-window sibling feature
This wiki is generated from the docs/ directory — edits made here are overwritten by the next sync.
To change a page, open a pull request against docs/.
Guides
- Development guide
- Releasing openstation
- Agents security model
- API Index
- Architecture
- Bridge protocol — wiring overview
- <os-*> component reference
- Native Desktop Host — Experimental
- Desktop themes
- Dock customization — two registries, one mental model
- The event-driven framework
- Files on the Desktop
- Folder sharing
- Getting Started
- Hooks Reference
- Icons
- JavaScript Reference
- The Living Tree — algorithm definition
- Mio
- Native Windows & Framework Interop
- Plugin compatibility layer
- Progressive Web App (PWA)
- Station Home
- Using openstation from your own plugin
Migration notes
- Migration: built-in activity channels move to the os/ namespace
- Migration: window, wallpaper and widget bundles load on demand
- Migration — the navigation model
- Migration: a native window's tabs move to the window chrome
All examples
- AI Agents — extend and invoke from a plugin
- wp.os.ai.ask() — programmatic AI Copilot
- Tune the AI model config
- Custom arrange-menu action
- Open a child window its owner can't cover
- Style a specific admin page inside the iframe
- Code Blue — register your plugin's log file
- Open a file in the Code editor (deep-link from any window)
- Connect to a window — title-bar button + iframe pub/sub
- Content changes — live-refresh every window listing your type
- Custom window chrome (Experimental)
- Register a custom unfocused-window effect
- Example: render a data table
- Real file storage — react to uploads, gate policy, share from PHP
- React to a window being set free onto the real desktop
- Cross-window devtools — instrumentation primitives
- Add a dock item with a badge
- Decorate the dock without forking the renderer
- Replace the dock rail entirely
- Retune the Drafts widget's AI writing assistant
- Gate OpenStation by role
- Iframe-initiated window opens
- Build a feed reader without the bookkeeping
- Inject data into openStationConfig
- Render a list without losing clicks — renderKeyedList()
- Example: layout primitives (body → panel → row → col)
- Use <os-*> components from a plugin that ships as a zip
- Restyle and drive Mio
- Add an action that works on a whole selection
- WP Explorer — custom post types and their folder
- Add an action button to a WP Explorer preview pane
- Example: native Posts window
- Example: native window with tabs
- Native windows
- Customize note → post conversion
- Send a notification
- OAuth relay — connect to an external service
- OS-file drop
- <os-flyout> — window-scoped sliding card
- Plugins window — extras
- Track who's around — wp.os.presence
- Example: progress bar
- PWA install — surface your own button
- React to window events
- Example: extend the Trash
- Register a slash-command
- Register a desktop theme from a plugin
- Register a game
- Example: register a desktop icon (Jorvy)
- Register a wallpaper
- Register a widget
- Related entities — extend the title bar's "Related" menu
- The native-window render ctx
- Programmatic folder sharing
- Share state across multi-bundle plugins — wp.os.createSharedStore()
- Example: loading spinner
- Add an opt-in card to Station Home
- Accept drops on your desktop icon
- Give a tile two icons, one per state
- Add a row to a window's ⋯ menu
- Example: window activity & the status ring
- Window controls
- Subscribe to window lifecycle events
- Window links — relate windows and restyle the ties (Experimental)
- Window loading state — spinner overlay & ready signal
- Show a banner at the top of a window
- Pulse a window's icon — Window.requestAttention()
- Register a custom window reveal
- Window slots
- Window themes
- Native window with bundle-bound config