-
Notifications
You must be signed in to change notification settings - Fork 41
example window theme
A theme is a named bag of CSS custom properties applied per-window. Two windows can carry different themes simultaneously — variables are written inline on each window's outer element, never to a global stylesheet, so they don't leak across windows.
Themes are part of the four-layer window-chrome customization framework:
| Layer | Surface | Status |
|---|---|---|
| 1 | Themes (this doc) | Stable |
| 2 | Controls — registerWindowControl
|
Stable |
| 3 | Slots — registerWindowSlot
|
Stable |
| 4 | Custom chrome render — registerWindowChrome
|
Experimental |
This doc covers Layer 1 only.
Designers can ship a theme with PHP alone. Register the theme with openstation_register_window_theme() and the shell applies the tokens to every window without further plumbing.
my-theme.php
<?php
/** Plugin Name: Midnight theme */
defined( 'ABSPATH' ) || exit;
add_action( 'init', function () {
openstation_register_window_theme( array(
'id' => 'my-theme/midnight',
'label' => __( 'Midnight', 'my-theme' ),
'tokens' => array(
'--os-window-bg' => '#11131b',
'--os-window-border' => '#2d3142',
'--os-window-radius' => '14px',
'--os-titlebar-bg' => '#1a1a2e',
'--os-titlebar-bg-focused' => '#252540',
'--os-titlebar-color' => '#a0a0b0',
'--os-titlebar-color-focused' => '#fafafa',
'--os-ui-btn-color' => '#fafafa',
'--os-ui-btn-bg-hover' => 'rgba(255,255,255,0.08)',
),
'priority' => 50,
) );
} );That's the entire plugin. The theme applies to every window, no JS round-trip needed.
When a theme should apply only to specific windows (the post editor, a single plugin's window, …), register it from JS with a match predicate.
my-theme.php
<?php
/** Plugin Name: Gutenberg neon */
defined( 'ABSPATH' ) || exit;
add_action( 'admin_enqueue_scripts', function () {
wp_register_script(
'gutenberg-neon-theme',
plugins_url( 'theme.js', __FILE__ ),
array( 'openstation' ),
'1.0.0',
true
);
wp_enqueue_script( 'gutenberg-neon-theme' );
} );
openstation_register_window_theme_script( 'gutenberg-neon-theme' );theme.js
wp.os.whenReady( () => {
wp.os.registerWindowTheme( {
id: 'gutenberg-neon/post-editor',
label: 'Neon Post Editor',
tokens: {
'--os-titlebar-bg-focused': '#ff00aa',
'--os-titlebar-color-focused': '#ffffff',
},
match: ( win ) => win.config.url?.includes( 'post.php' ) ?? false,
owner: 'gutenberg-neon-theme', // for live unregister on deactivation
} );
} );The owner field is the WP script handle. When the plugin deactivates, the chrome server-sync drops this theme without requiring a page reload.
When the window registrant owns the theme, declare it inline via WindowConfig.appearance.theme instead of registering globally:
wp.os.registerWindow( {
id: 'my-plugin/dashboard',
title: 'Dashboard',
icon: 'dashicons-dashboard',
width: 640,
height: 480,
minWidth: 320,
minHeight: 200,
appearance: {
theme: {
tokens: {
'--os-titlebar-bg-focused': '#0066cc',
'--os-titlebar-color-focused': '#ffffff',
},
},
},
render: ( body ) => { body.textContent = 'Hello'; },
} );Or pin a registered theme by id:
appearance: { theme: { themeId: 'my-theme/midnight' } }wp.os.applyWindowTheme() re-themes a live window. Useful for theme pickers, dark-mode toggles, or onboarding tours that flash a window highlight.
wp.os.applyWindowTheme( 'edit-post', {
tokens: { '--os-titlebar-bg-focused': '#ff8800' },
} );
// Or pin a registered theme by id:
wp.os.applyWindowTheme( 'edit-post', 'my-theme/midnight' );
// Or clear the override and fall back to the registry:
wp.os.applyWindowTheme( 'edit-post', null );The override is also written into the window's config.appearance.theme so subsequent registry-driven re-applies preserve the runtime choice.
| Hook | Type | Signature | Purpose |
|---|---|---|---|
openstation_window_theme_script_registered |
action | ( string $handle ) |
Fires after openstation_register_window_theme_script() succeeds. |
openstation_window_theme_registered |
action | ( string $id, array $entry ) |
Fires after openstation_register_window_theme() stores metadata. |
| Hook | Type | Signature | Purpose |
|---|---|---|---|
os.window.chrome.theme |
filter | ( tokens, { windowId, themeId, config } ) => tokens |
Mutate the resolved CSS-variable map for any window before it's written to the element. Stable. |
os.window.chrome.theme-changed |
action | ( { windowId, themeId, tokens } ) |
Fires after each successful apply. Stable. |
CSS-variable contract that themes can override: see assets/css/window-chrome.css for the canonical list. The most commonly themed:
-
--os-window-bg,--os-window-border,--os-window-radius,--os-window-shadow,--os-window-shadow-focused -
--os-titlebar-bg,--os-titlebar-bg-focused,--os-titlebar-color,--os-titlebar-color-focused,--os-titlebar-height -
--os-ui-btn-color,--os-ui-btn-bg-hover,--os-ui-btn-outline,--os-ui-btn-danger-hover
A non-CSS-variable key (one that doesn't start with --) is rejected at registration time with a RegistrationError — the framework refuses to write anything to the element that isn't a custom property.
When the plugin's WordPress script handle leaves the next live-refresh payload (deactivation), the chrome server-sync tears down the plugin's themes:
- Themes registered via PHP metadata (
openstation_register_window_theme()) are dropped automatically — their handle ↔ id mapping is captured in the previous payload snapshot. - Themes registered from JS with
owner: '<script-handle>'are also dropped automatically. - Themes registered from JS without
ownersurvive until the next page reload (graceful backwards-compat).
Open windows repaint live via the registry's subscribe fan-out — no F5 required.
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