{/* @wedevs/plugin-ui — Developer Guide (Storybook) */}
import { Meta } from "@storybook/addon-docs/blocks";
A ShadCN-style React component library built for WordPress plugins. Provides 50+ themed, accessible UI components powered by Tailwind CSS v4, @base-ui/react headless primitives, and first-class WordPress integration.
- Quick Start
- Prerequisites
- Installation
- Project Setup
- ThemeProvider
- Component Catalog
- WordPress Integration
- Pro Plugin / Add-on Architecture
- Utilities & Hooks
import { ThemeProvider, Button, Card, CardHeader, CardTitle, CardContent } from '@wedevs/plugin-ui';
import '@wedevs/plugin-ui/styles.css';
function App() {
return (
<ThemeProvider pluginId="my-plugin">
<Card>
<CardHeader>
<CardTitle>Hello World</CardTitle>
</CardHeader>
<CardContent>
<Button>Click me</Button>
</CardContent>
</Card>
</ThemeProvider>
);
}| Tool | Version |
|---|---|
| Node.js | 18+ |
| npm | 9+ |
| WordPress | 6.4+ |
| @wordpress/scripts | 28+ |
| React | 18.2+ |
| Tailwind CSS | 4.x |
# From the same monorepo / adjacent directory
npm install @wedevs/plugin-ui@file:../plugin-ui
# Or from npm (when published)
npm install @wedevs/plugin-ui{
"name": "my-wordpress-plugin",
"dependencies": {
"@wedevs/plugin-ui": "file:../plugin-ui",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^7.6.1"
},
"devDependencies": {
"@tailwindcss/postcss": "^4.1.0",
"@wordpress/scripts": "^28.0.0",
"postcss": "^8.4.47",
"tailwindcss": "^4.1.0"
},
"scripts": {
"build": "wp-scripts build",
"start": "wp-scripts start"
}
}Use @wordpress/scripts as the base, with custom entry points:
// webpack.config.js
const defaultConfig = require('@wordpress/scripts/config/webpack.config');
const path = require('path');
module.exports = {
...defaultConfig,
entry: {
'my-plugin-app': path.resolve(__dirname, 'src/index.tsx'),
},
output: {
...defaultConfig.output,
filename: '[name].js',
},
resolve: {
...defaultConfig.resolve,
alias: {
...defaultConfig.resolve?.alias,
'@': path.resolve(__dirname, 'src'),
},
extensions: [
...(defaultConfig.resolve?.extensions || []),
'.ts', '.tsx',
],
},
};// postcss.config.js
module.exports = {
plugins: {
'@tailwindcss/postcss': {},
},
};{
"compilerOptions": {
"target": "ES5",
"module": "ESNext",
"moduleResolution": "node",
"jsx": "react-jsx",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"paths": { "@/*": ["./src/*"] }
},
"include": ["src/**/*"],
"exclude": ["node_modules", "build"]
}This is the most critical configuration step. The layered approach scopes Tailwind's preflight reset and utilities to your app container, preventing style conflicts with WordPress.
/* src/styles/main.css */
/* 1. Declare layer order for deterministic specificity */
@layer theme, base, components, utilities;
/* 2. Import Tailwind theme (design tokens layer) */
@import "tailwindcss/theme.css" layer(theme);
/* 3. Scan your source files and plugin-ui for Tailwind classes */
@source "../../src";
@source "../../../node_modules/@wedevs/plugin-ui/dist";
/* 4. Scope preflight (CSS reset) and utilities to your app container.
This prevents Tailwind from resetting styles on the rest of the page.
Replace #my-plugin-app with your actual mount-point ID. */
#my-plugin-app {
@import "tailwindcss/preflight.css" layer(base);
@import "tailwindcss/utilities.css" layer(utilities) important;
}
/* 5. Import plugin-ui component styles */
@import '@wedevs/plugin-ui/styles.css';
/* 6. Define your theme tokens on .pui-root (set by ThemeProvider) */
.pui-root {
--background: oklch(1 0 0);
--foreground: oklch(0.1450 0 0);
--primary: oklch(.511 .262 276.966);
--primary-foreground: oklch(0.9850 0 0);
/* ... other token overrides ... */
--radius: 0.625rem;
}
/* 7. Map CSS variables to Tailwind theme tokens */
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-destructive-foreground: var(--destructive-foreground);
--color-success: var(--success);
--color-success-foreground: var(--success-foreground);
--color-warning: var(--warning);
--color-warning-foreground: var(--warning-foreground);
--color-info: var(--info);
--color-info-foreground: var(--info-foreground);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--color-chart-1: var(--chart-1);
--color-chart-2: var(--chart-2);
--color-chart-3: var(--chart-3);
--color-chart-4: var(--chart-4);
--color-chart-5: var(--chart-5);
--color-sidebar: var(--sidebar);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring);
--font-sans: var(--font-sans);
--font-serif: var(--font-serif);
--font-mono: var(--font-mono);
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
--radius-2xl: calc(var(--radius) + 8px);
--shadow-2xs: var(--shadow-2xs);
--shadow-xs: var(--shadow-xs);
--shadow-sm: var(--shadow-sm);
--shadow: var(--shadow);
--shadow-md: var(--shadow-md);
--shadow-lg: var(--shadow-lg);
--shadow-xl: var(--shadow-xl);
--shadow-2xl: var(--shadow-2xl);
}Why layered imports? Scoping preflight and utilities to your container (
#my-plugin-app) prevents Tailwind's CSS reset from interfering with WordPress admin styles. Theimportantkeyword ensures your utility classes win over WordPress defaults within the scope.Portal-based components (Modals, Popovers): plugin-ui's
Modalcomponent automatically creates a.pui-rootportal container ondocument.body. If you use Tailwind utility classes inside portaled content, add.pui-rootas an additional scope:#my-plugin-app, .pui-root { @import "tailwindcss/preflight.css" layer(base); @import "tailwindcss/utilities.css" layer(utilities) important; }Why is
@theme inlineneeded? Tailwind v4 needs to know about your CSS variables to generate utilities likebg-primary,text-muted-foreground, etc. The@theme inlineblock maps--primary(set by ThemeProvider) to--color-primary(used by Tailwind).
Register and enqueue the built JS/CSS in your WordPress plugin:
<?php
namespace MyPlugin;
class Assets {
public function __construct() {
add_action('admin_enqueue_scripts', [$this, 'enqueue_scripts']);
}
public function enqueue_scripts($hook) {
// Only load on your plugin page
if ($hook !== 'toplevel_page_my-plugin') {
return;
}
$asset_file = MY_PLUGIN_PATH . '/build/my-plugin-app.asset.php';
$asset_data = file_exists($asset_file) ? include $asset_file : [];
$dependencies = $asset_data['dependencies'] ?? [];
$version = $asset_data['version'] ?? MY_PLUGIN_VERSION;
// Enqueue the JavaScript bundle
wp_enqueue_script(
'my-plugin-app',
MY_PLUGIN_URL . '/build/my-plugin-app.js',
$dependencies,
$version,
true
);
// Enqueue the CSS bundle
wp_enqueue_style(
'my-plugin-app',
MY_PLUGIN_URL . '/build/my-plugin-app.css',
['wp-components'],
$version
);
// Pass data to JavaScript
wp_localize_script(
'my-plugin-app',
'myPluginData',
[
'rest_url' => esc_url_raw(get_rest_url()),
'nonce' => wp_create_nonce('wp_rest'),
]
);
}
}Create a container div for React to mount into:
<!-- admin-page.php -->
<div id="my-plugin-app" class="wrap"></div>Note:
@wordpress/scriptsautomatically generates the.asset.phpfile with correct WordPress dependencies during build.
// src/index.tsx
import React from 'react';
import { createRoot } from 'react-dom/client';
import { ThemeProvider, type ThemeTokens } from '@wedevs/plugin-ui';
import { HashRouter } from 'react-router-dom';
import App from './App';
import './styles/main.css';
const myThemeTokens: ThemeTokens = {
primary: 'oklch(.511 .262 276.966)',
primaryForeground: 'oklch(0.9850 0 0)',
radius: '0.625rem',
};
const container = document.getElementById('my-plugin-app');
if (container) {
const root = createRoot(container);
root.render(
<ThemeProvider pluginId="my-plugin" tokens={myThemeTokens}>
<HashRouter>
<App />
</HashRouter>
</ThemeProvider>
);
}The ThemeProvider wraps your app and injects CSS custom properties under the .pui-root scope. All plugin-ui components read these variables for their styles.
import { ThemeProvider } from '@wedevs/plugin-ui';
<ThemeProvider
pluginId="my-plugin" // Required: unique CSS scope identifier
tokens={lightTokens} // Optional: light theme overrides
darkTokens={darkTokens} // Optional: dark theme overrides
defaultMode="light" // Optional: "light" | "dark" | "system"
mode="light" // Optional: controlled mode
storageKey="my-theme" // Optional: localStorage key for persistence
>
{children}
</ThemeProvider>Access the theme in children:
import { useTheme } from '@wedevs/plugin-ui';
function ThemeToggle() {
const { mode, setMode } = useTheme();
return (
<button onClick={() => setMode(mode === 'light' ? 'dark' : 'light')}>
Toggle: {mode}
</button>
);
}All tokens use the OKLCh color space for perceptually uniform colors:
| Token | Description |
|---|---|
background / foreground | Page background and text |
card / cardForeground | Card surfaces |
popover / popoverForeground | Dropdown/popover surfaces |
primary / primaryForeground | Primary brand color |
secondary / secondaryForeground | Secondary color |
muted / mutedForeground | Muted/disabled state |
accent / accentForeground | Accent highlights |
destructive / destructiveForeground | Danger/delete actions |
success / successForeground | Success state |
warning / warningForeground | Warning state |
info / infoForeground | Informational state |
border | Default border color |
input | Input border color |
ring | Focus ring color |
chart1–chart5 | Chart/graph colors |
sidebar* | Sidebar-specific colors |
radius | Base border radius |
fontSans / fontSerif / fontMono | Font families |
shadow* | Shadow values |
Pass both tokens and darkTokens to enable dark mode:
const lightTokens: ThemeTokens = {
background: 'oklch(1 0 0)',
foreground: 'oklch(0.1450 0 0)',
primary: 'oklch(.511 .262 276.966)',
};
const darkTokens: ThemeTokens = {
background: 'oklch(0.1450 0 0)',
foreground: 'oklch(0.9850 0 0)',
primary: 'oklch(0.9220 0 0)',
};
<ThemeProvider
pluginId="my-plugin"
tokens={lightTokens}
darkTokens={darkTokens}
defaultMode="system"
>
<App />
</ThemeProvider>import { defaultTheme, defaultDarkTheme, twitterTheme, cyberpunkTheme } from '@wedevs/plugin-ui';
<ThemeProvider pluginId="my-plugin" tokens={twitterTheme} darkTokens={defaultDarkTheme}>Available presets: defaultTheme, slateTheme, amberMinimalTheme, t3ChatTheme, midnightBloomTheme, bubblegumTheme, cyberpunkTheme, twitterTheme — each with a *DarkTheme variant.
import { Button } from '@wedevs/plugin-ui';
<Button variant="default" size="default">Save</Button>
<Button variant="destructive" size="sm">Delete</Button>
<Button variant="outline" size="icon"><Icon /></Button>
<Button variant="ghost">Cancel</Button>
<Button variant="success">Confirm</Button>
<Button progress={75}>Uploading...</Button>Variants: default, secondary, outline, ghost, destructive, outline-destructive, success, outline-success, link
Sizes: default, xs, sm, lg, icon, icon-xs, icon-sm, icon-lg
import { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter, CardAction } from '@wedevs/plugin-ui';
<Card>
<CardHeader>
<CardTitle>Title</CardTitle>
<CardDescription>Description</CardDescription>
<CardAction><Button variant="ghost" size="icon-sm"><MoreIcon /></Button></CardAction>
</CardHeader>
<CardContent>Content</CardContent>
<CardFooter>Footer</CardFooter>
</Card>import { Tabs, TabsList, TabsTrigger, TabsContent } from '@wedevs/plugin-ui';
<Tabs defaultValue="general">
<TabsList>
<TabsTrigger value="general">General</TabsTrigger>
<TabsTrigger value="advanced">Advanced</TabsTrigger>
</TabsList>
<TabsContent value="general">General settings...</TabsContent>
<TabsContent value="advanced">Advanced settings...</TabsContent>
</Tabs>import { Separator, ScrollArea, ScrollBar } from '@wedevs/plugin-ui';
<Separator />
<Separator orientation="vertical" />
<ScrollArea className="h-72 w-48">
<div>Long content...</div>
<ScrollBar orientation="vertical" />
</ScrollArea>import { Input, InputGroup, InputGroupAddon, InputGroupInput, InputGroupButton } from '@wedevs/plugin-ui';
<Input type="text" placeholder="Enter name" />
<InputGroup>
<InputGroupAddon>https://</InputGroupAddon>
<InputGroupInput placeholder="example.com" />
<InputGroupButton>Go</InputGroupButton>
</InputGroup>import { CurrencyInput } from '@wedevs/plugin-ui';
<CurrencyInput
value={amount}
onChange={(val) => setAmount(val)}
currency="USD"
currencyOptions={[
{ value: 'USD', label: '$' },
{ value: 'EUR', label: '€' },
]}
/>import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from '@wedevs/plugin-ui';
<Select value={value} onValueChange={setValue}>
<SelectTrigger><SelectValue placeholder="Choose..." /></SelectTrigger>
<SelectContent>
<SelectItem value="opt1">Option 1</SelectItem>
<SelectItem value="opt2">Option 2</SelectItem>
</SelectContent>
</Select>import { Combobox, ComboboxTrigger, ComboboxContent, ComboboxInput, ComboboxItem, ComboboxChips } from '@wedevs/plugin-ui';
<Combobox multiple value={selected} onValueChange={setSelected}>
<ComboboxTrigger>
<ComboboxChips />
<ComboboxInput placeholder="Search..." />
</ComboboxTrigger>
<ComboboxContent>
<ComboboxItem value="react">React</ComboboxItem>
<ComboboxItem value="vue">Vue</ComboboxItem>
</ComboboxContent>
</Combobox>import { LabeledCheckbox, CheckboxCard } from '@wedevs/plugin-ui';
import { RadioGroup, LabeledRadio, RadioCard } from '@wedevs/plugin-ui';
import { Switch, LabeledSwitch, SwitchCard } from '@wedevs/plugin-ui';
<LabeledCheckbox label="Accept terms" description="Read the full terms" />
<CheckboxCard label="Premium" description="Enable premium features" />
<RadioGroup value={value} onValueChange={setValue}>
<LabeledRadio value="flat" label="Flat Rate" description="Single rate" />
<LabeledRadio value="percent" label="Percentage" description="Based on total" />
</RadioGroup>
<LabeledSwitch label="Dark Mode" description="Toggle dark theme" />
<SwitchCard label="Notifications" description="Enable email notifications" />import { Slider, ColorPicker } from '@wedevs/plugin-ui';
<Slider value={[50]} onChange={(val) => setValue(val)} min={0} max={100} step={1} />
<ColorPicker value={color} onChange={setColor} placeholder="Pick a color" />import { Field, FieldLabel, FieldContent, FieldDescription, FieldError } from '@wedevs/plugin-ui';
<Field orientation="horizontal">
<FieldLabel required>Store Name</FieldLabel>
<FieldContent>
<Input value={name} onChange={e => setName(e.target.value)} />
<FieldDescription>Your public store name</FieldDescription>
<FieldError>This field is required</FieldError>
</FieldContent>
</Field>import { Avatar, AvatarImage, AvatarFallback, Badge } from '@wedevs/plugin-ui';
<Avatar>
<AvatarImage src="/photo.jpg" alt="User" />
<AvatarFallback>JD</AvatarFallback>
</Avatar>
<Badge>New</Badge>
<Badge variant="secondary">Draft</Badge>
<Badge variant="destructive">Expired</Badge>import { Breadcrumb, BreadcrumbList, BreadcrumbItem, BreadcrumbLink, BreadcrumbSeparator, BreadcrumbPage } from '@wedevs/plugin-ui';
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem><BreadcrumbLink href="/">Home</BreadcrumbLink></BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem><BreadcrumbPage>Settings</BreadcrumbPage></BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>import { Skeleton, Progress, ProgressTrack, ProgressIndicator, CircularProgress, Spinner } from '@wedevs/plugin-ui';
<Skeleton className="h-4 w-[200px]" />
<Progress value={65}><ProgressTrack><ProgressIndicator /></ProgressTrack></Progress>
<CircularProgress value={75} />
<Spinner />import { MatricsCard } from '@wedevs/plugin-ui';
<MatricsCard
icon={<DollarSign />}
value="$12,450"
count="+12%"
countDirection="up"
shortDescription="Total Revenue"
tooltip="Revenue from last 30 days"
/>import { ChartContainer, ChartTooltip, ChartTooltipContent, recharts } from '@wedevs/plugin-ui';
const { BarChart, Bar, XAxis, YAxis } = recharts;
<ChartContainer config={{ revenue: { label: 'Revenue', color: 'var(--chart-1)' } }}>
<BarChart data={data}>
<XAxis dataKey="month" />
<YAxis />
<Bar dataKey="revenue" fill="var(--chart-1)" />
<ChartTooltip content={<ChartTooltipContent />} />
</BarChart>
</ChartContainer>import { Modal, ModalContent, ModalHeader, ModalTitle, ModalDescription, ModalFooter, ModalClose } from '@wedevs/plugin-ui';
<Modal open={open} onOpenChange={setOpen}>
<ModalContent>
<ModalHeader>
<ModalTitle>Confirm Action</ModalTitle>
<ModalDescription>This cannot be undone.</ModalDescription>
</ModalHeader>
<ModalFooter>
<ModalClose asChild><Button variant="outline">Cancel</Button></ModalClose>
<Button variant="destructive" onClick={handleDelete}>Delete</Button>
</ModalFooter>
</ModalContent>
</Modal>import { AlertDialog, AlertDialogTrigger, AlertDialogContent, AlertDialogHeader, AlertDialogTitle,
AlertDialogDescription, AlertDialogFooter, AlertDialogAction, AlertDialogCancel } from '@wedevs/plugin-ui';
<AlertDialog>
<AlertDialogTrigger asChild><Button variant="destructive">Delete</Button></AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Are you sure?</AlertDialogTitle>
<AlertDialogDescription>This action cannot be undone.</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction>Continue</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>import { Sheet, SheetTrigger, SheetContent, SheetHeader, SheetTitle } from '@wedevs/plugin-ui';
import { Popover, PopoverTrigger, PopoverContent } from '@wedevs/plugin-ui';
import { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem } from '@wedevs/plugin-ui';
<Sheet>
<SheetTrigger asChild><Button>Open Panel</Button></SheetTrigger>
<SheetContent side="right">
<SheetHeader><SheetTitle>Details</SheetTitle></SheetHeader>
<div>Panel content...</div>
</SheetContent>
</Sheet>
<Popover>
<PopoverTrigger asChild><Button variant="outline">Options</Button></PopoverTrigger>
<PopoverContent><p>Popover content</p></PopoverContent>
</Popover>
<DropdownMenu>
<DropdownMenuTrigger asChild><Button variant="ghost">Menu</Button></DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem>Edit</DropdownMenuItem>
<DropdownMenuItem>Duplicate</DropdownMenuItem>
<DropdownMenuItem className="text-destructive">Delete</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>import { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } from '@wedevs/plugin-ui';
<TooltipProvider>
<Tooltip>
<TooltipTrigger><InfoIcon /></TooltipTrigger>
<TooltipContent>Helpful information</TooltipContent>
</Tooltip>
</TooltipProvider>import { Alert, AlertTitle, AlertDescription } from '@wedevs/plugin-ui';
import { Notice, NoticeTitle, NoticeAction } from '@wedevs/plugin-ui';
<Alert variant="destructive">
<AlertTitle>Error</AlertTitle>
<AlertDescription>Something went wrong.</AlertDescription>
</Alert>
<Notice variant="info">
<NoticeTitle>Update Available</NoticeTitle>
<NoticeAction><Button size="sm">Update Now</Button></NoticeAction>
</Notice>A pre-built WordPress admin layout with responsive sidebar:
import {
Layout, LayoutBody, LayoutSidebar, LayoutMain, LayoutHeader,
LayoutMenu, useLayout
} from '@wedevs/plugin-ui';
function AdminPage() {
const menuItems = [
{ id: 'dashboard', label: 'Dashboard', icon: <HomeIcon /> },
{ id: 'orders', label: 'Orders', icon: <ShoppingCartIcon />, badge: 5 },
{
id: 'settings', label: 'Settings', icon: <SettingsIcon />,
children: [
{ id: 'general', label: 'General' },
{ id: 'payments', label: 'Payments' },
],
},
];
return (
<Layout namespace="my-plugin">
<LayoutBody>
<LayoutSidebar collapsible="icon" variant="sidebar">
<LayoutMenu
items={menuItems}
activeId={activePageId}
onSelect={(id) => navigate(`/${id}`)}
searchable
/>
</LayoutSidebar>
<LayoutMain>
<LayoutHeader>Page Title</LayoutHeader>
<Outlet />
</LayoutMain>
</LayoutBody>
</Layout>
);
}The namespace prop integrates with @wordpress/hooks for extensibility.
A schema-driven settings page built for WordPress:
import { Settings, type SettingsElement } from '@wedevs/plugin-ui';
import { applyFilters } from '@wordpress/hooks';
import { __ } from '@wordpress/i18n';
const schema: SettingsElement[] = [
{
id: 'general', type: 'page', label: __('General', 'my-plugin'),
children: [{
id: 'store', type: 'subpage', label: __('Store', 'my-plugin'), icon: 'Store',
children: [{
id: 'basic_section', type: 'section', label: __('Basic Settings', 'my-plugin'),
children: [
{ id: 'store_name', type: 'field', variant: 'text', label: __('Store Name', 'my-plugin'), dependency_key: 'store_name', default: '' },
{ id: 'enable_tax', type: 'field', variant: 'switch', label: __('Enable Tax', 'my-plugin'), dependency_key: 'enable_tax', default: false },
{ id: 'tax_rate', type: 'field', variant: 'number', label: __('Tax Rate (%)', 'my-plugin'), dependency_key: 'tax_rate',
dependencies: [{ key: 'enable_tax', value: true, comparison: '=' }] },
],
}],
}],
},
];
function SettingsPage() {
const [values, setValues] = useState({});
return (
<Settings
schema={schema}
values={values}
title={__('My Plugin', 'my-plugin')}
hookPrefix="my_plugin"
applyFilters={applyFilters}
onChange={(scopeId, key, value) => setValues(prev => ({ ...prev, [key]: value }))}
onSave={async (scopeId, treeValues) => {
await apiFetch({ path: `/my-plugin/v1/settings/${scopeId}`, method: 'POST', data: treeValues });
}}
renderSaveButton={({ dirty, onSave }) => (
<Button onClick={onSave} disabled={!dirty}>{__('Save Changes', 'my-plugin')}</Button>
)}
/>
);
}Field variants: text, number, textarea, select, switch, radio_capsule, customize_radio, multicheck, checkbox_group, base_field_label, html.
See the Settings / Documentation page in Storybook for full documentation.
import { FileUpload } from '@wedevs/plugin-ui';
<FileUpload
text="Upload Logo"
description="Recommended: 200x200px PNG"
onUpload={(media) => {
if (media) setLogo(media.url);
}}
variant="button"
/>Opens the native WordPress Media Library modal.
Components auto-detect WordPress locale and timezone:
import { Calendar, DatePicker, DateRangePicker } from '@wedevs/plugin-ui';
<Calendar mode="single" selected={date} onSelect={setDate} />
<DatePicker value={date} onChange={setDate} placeholder="Select date" />
<DateRangePicker mode="range" value={range} onChange={setRange} />WordPress date utilities:
import { getWordPressTimezone, getWordPressLocale, formatWordPressDate } from '@wedevs/plugin-ui';
const tz = getWordPressTimezone(); // "America/New_York"
const locale = getWordPressLocale(); // "en_US"
const formatted = formatWordPressDate(new Date());import { DataViews } from '@wedevs/plugin-ui';
<div className="pui-root">
<DataViews
data={orders}
fields={[
{ id: 'id', header: 'ID' },
{ id: 'customer', header: 'Customer' },
{ id: 'total', header: 'Total', render: ({ item }) => `$${item.total}` },
]}
view={view}
onChangeView={setView}
/>
</div>Note: Wrap DataViews in a
<div className="pui-root">to ensure proper styling scope.
This section explains how to build an add-on plugin that extends a base plugin using shared plugin-ui components (as demonstrated by wepos/wepos-pro).
The base plugin bundles @wedevs/plugin-ui and exposes it as a global. Add-on plugins reference it as external:
Base plugin entry:
import * as PluginUI from '@wedevs/plugin-ui';
Object.assign((window as any).__myPluginUI, PluginUI);Base plugin PHP (before any bundles load):
wp_add_inline_script(
'wp-hooks',
'window.__myPluginUI = window.__myPluginUI || {};',
'after'
);Add-on webpack.config.js:
externals: {
...defaultConfig.externals,
'@wedevs/plugin-ui': '__myPluginUI',
},Add-on PHP — dependency ordering:
// Ensure add-on loads BEFORE base so hooks register first
$wp_scripts = wp_scripts();
if (isset($wp_scripts->registered['base-plugin-app'])) {
$wp_scripts->registered['base-plugin-app']->deps[] = 'addon-plugin-app';
}// addon/src/index.tsx
import { Button } from '@wedevs/plugin-ui';
import { addFilter } from './hooks';
addFilter('my_plugin_routes', 'addon/routes', (routes) => [
...routes,
{ path: '/premium-feature', element: <PremiumPage /> },
]);
addFilter('my_plugin_sidebar_items', 'addon/sidebar', (items) => [
...items,
{ id: 'premium', label: 'Premium', icon: <CrownIcon /> },
]);import { cn } from '@wedevs/plugin-ui';
<div className={cn('p-4 rounded-lg', isActive && 'bg-primary text-primary-foreground')} />import { useIsMobile } from '@wedevs/plugin-ui';
const isMobile = useIsMobile(); // true if viewport < 768pximport { useWindowDimensions } from '@wedevs/plugin-ui';
const { width, height } = useWindowDimensions();import { renderIcon } from '@wedevs/plugin-ui';
renderIcon(Settings, { size: 16 }) // <Settings size={16} />
renderIcon(<Settings />, { size: 16 }) // <Settings />// Recharts
import { recharts } from '@wedevs/plugin-ui';
const { LineChart, Line, XAxis, YAxis } = recharts;
// React Day Picker
import { ReactDayPicker } from '@wedevs/plugin-ui';
const { DayPicker } = ReactDayPicker;my-plugin/
├── build/ # wp-scripts output
│ ├── my-plugin-app.js
│ ├── my-plugin-app.css
│ └── my-plugin-app.asset.php
├── includes/
│ └── Assets.php # PHP enqueue class
├── src/
│ ├── index.tsx # React entry point
│ ├── App.tsx # Routes & layout
│ ├── components/ # Your React components
│ ├── pages/ # Route pages
│ ├── hooks/ # Custom hooks
│ ├── store/ # @wordpress/data stores
│ ├── api/ # REST API calls
│ └── styles/
│ └── main.css # Tailwind + plugin-ui imports
├── webpack.config.js
├── postcss.config.js
├── tsconfig.json
├── package.json
└── my-plugin.php # Plugin main file