Skip to content

Latest commit

 

History

History
1129 lines (906 loc) · 31.6 KB

File metadata and controls

1129 lines (906 loc) · 31.6 KB

{/* @wedevs/plugin-ui — Developer Guide (Storybook) */}

import { Meta } from "@storybook/addon-docs/blocks";

@wedevs/plugin-ui — Developer Guide

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.


Table of Contents


Quick Start

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>
  );
}

Prerequisites

ToolVersion
Node.js18+
npm9+
WordPress6.4+
@wordpress/scripts28+
React18.2+
Tailwind CSS4.x

Installation

# From the same monorepo / adjacent directory
npm install @wedevs/plugin-ui@file:../plugin-ui

# Or from npm (when published)
npm install @wedevs/plugin-ui

Project Setup

1. package.json

{
  "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"
  }
}

2. Webpack Configuration

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',
    ],
  },
};

3. PostCSS Configuration

// postcss.config.js
module.exports = {
  plugins: {
    '@tailwindcss/postcss': {},
  },
};

4. TypeScript Configuration

{
  "compilerOptions": {
    "target": "ES5",
    "module": "ESNext",
    "moduleResolution": "node",
    "jsx": "react-jsx",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "paths": { "@/*": ["./src/*"] }
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "build"]
}

5. CSS Setup (Tailwind v4)

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. The important keyword ensures your utility classes win over WordPress defaults within the scope.

Portal-based components (Modals, Popovers): plugin-ui's Modal component automatically creates a .pui-root portal container on document.body. If you use Tailwind utility classes inside portaled content, add .pui-root as an additional scope:

#my-plugin-app,
.pui-root {
  @import "tailwindcss/preflight.css" layer(base);
  @import "tailwindcss/utilities.css" layer(utilities) important;
}

Why is @theme inline needed? Tailwind v4 needs to know about your CSS variables to generate utilities like bg-primary, text-muted-foreground, etc. The @theme inline block maps --primary (set by ThemeProvider) to --color-primary (used by Tailwind).

6. PHP Asset Enqueuing

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/scripts automatically generates the .asset.php file with correct WordPress dependencies during build.

7. React Entry Point

// 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>
  );
}

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>
  );
}

Theme Tokens

All tokens use the OKLCh color space for perceptually uniform colors:

TokenDescription
background / foregroundPage background and text
card / cardForegroundCard surfaces
popover / popoverForegroundDropdown/popover surfaces
primary / primaryForegroundPrimary brand color
secondary / secondaryForegroundSecondary color
muted / mutedForegroundMuted/disabled state
accent / accentForegroundAccent highlights
destructive / destructiveForegroundDanger/delete actions
success / successForegroundSuccess state
warning / warningForegroundWarning state
info / infoForegroundInformational state
borderDefault border color
inputInput border color
ringFocus ring color
chart1chart5Chart/graph colors
sidebar*Sidebar-specific colors
radiusBase border radius
fontSans / fontSerif / fontMonoFont families
shadow*Shadow values

Dark Mode

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>

Built-in Theme Presets

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.


Component Catalog

Layout Components

Button

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

Card

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>

Tabs

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>

Separator & ScrollArea

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>

Form Components

Input & InputGroup

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>

CurrencyInput

import { CurrencyInput } from '@wedevs/plugin-ui';

<CurrencyInput
  value={amount}
  onChange={(val) => setAmount(val)}
  currency="USD"
  currencyOptions={[
    { value: 'USD', label: '$' },
    { value: 'EUR', label: '€' },
  ]}
/>

Select

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>

Combobox (Multi-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>

Checkbox, RadioGroup, Switch

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" />

Slider & ColorPicker

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" />

Field (Form Layout)

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>

Data Display

Avatar & Badge

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>

Breadcrumb

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>

Skeleton, Progress & Spinner

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 />

MetricsCard

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"
/>

Chart (Recharts Wrapper)

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>

Overlay Components

Modal (Dialog)

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>

AlertDialog

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>

Sheet, Popover & DropdownMenu

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>

Tooltip

import { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } from '@wedevs/plugin-ui';

<TooltipProvider>
  <Tooltip>
    <TooltipTrigger><InfoIcon /></TooltipTrigger>
    <TooltipContent>Helpful information</TooltipContent>
  </Tooltip>
</TooltipProvider>

Feedback

Alert & Notice

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>

WordPress Integration

Layout System

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.

Settings Page

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.

File Upload (WP Media)

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.

Date/Calendar with WP Locale

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());

DataViews

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.


Pro Plugin / Add-on Architecture

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).

Sharing plugin-ui via Webpack Externals

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';
}

Hook-based Extension Pattern

// 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 /> },
]);

Utilities & Hooks

cn() — Class Name Merger

import { cn } from '@wedevs/plugin-ui';

<div className={cn('p-4 rounded-lg', isActive && 'bg-primary text-primary-foreground')} />

useIsMobile()

import { useIsMobile } from '@wedevs/plugin-ui';
const isMobile = useIsMobile(); // true if viewport < 768px

useWindowDimensions()

import { useWindowDimensions } from '@wedevs/plugin-ui';
const { width, height } = useWindowDimensions();

renderIcon()

import { renderIcon } from '@wedevs/plugin-ui';
renderIcon(Settings, { size: 16 })     // <Settings size={16} />
renderIcon(<Settings />, { size: 16 }) // <Settings />

Re-exported Libraries

// 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;

Directory Structure Reference

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