@projectdiscoveryio/design-system
v1.0.2
Published
Production-grade design system with adapter layer support
Readme
@pd-design/system
Production-grade design system with adapter layer support. Built for teams who need a flexible, type-safe component library that can work with different UI engines without changing application code.
About PD Design System
- 3 Lines to Get Started - No complex setup, just works
- System Mode Built-In - Automatically follows OS dark/light preference
- Zero-Config Persistence - Pass
storageKeyprop, localStorage sync is automatic - Cross-Tab Sync - Theme changes sync across browser tabs instantly
- Type-Safe - Full TypeScript with autocomplete for all props
- Accessible - WCAG 2.1 AA compliant, built on Radix UI
- Style Isolation - Works with Tailwind, MUI, AntD, Bootstrap, or plain CSS
- Extensible Adapters - Switch between UI engines (shadcn, Material, AntD, etc.)
- Flexible Theming - Base theme with support for custom brand themes
- 1000+ Icons - All Lucide icons with TypeScript autocomplete
Table of Contents
- Installation
- Quick Start
- Implementation Guide
- Design System Architecture
- Adapters
- Theming System
- Mode System
- Component Usage
- API Reference
- Advanced Usage
Installation
Install the package using npm, yarn, or pnpm:
npm install @pd-design/system
# or
yarn add @pd-design/system
# or
pnpm add @pd-design/systemPeer Dependencies
The design system requires React 18+:
npm install react@^18.0.0 react-dom@^18.0.0Quick Start
Get started in 3 simple steps:
Step 1: Import Styles
Import the design system styles in your application entry point (e.g., main.tsx, App.tsx, or _app.tsx):
import '@pd-design/system/styles.css';Step 2: Wrap Your App with ThemeProvider
Wrap your application with ThemeProvider to enable theming and component functionality:
import { ThemeProvider } from '@pd-design/system';
function App() {
return (
<ThemeProvider>
{/* Your application components */}
</ThemeProvider>
);
}That's it! The ThemeProvider uses sensible defaults:
- Adapter:
'shadcn'(Radix UI + Tailwind CSS) - Theme:
'base'(Neutral, professional design) - Mode:
'system'(Automatically follows OS dark/light preference)
Optional Configuration
You can customize the theme, mode, adapter, and enable persistence:
import { ThemeProvider } from '@pd-design/system';
function App() {
return (
<ThemeProvider
adapter="shadcn" // Optional: 'shadcn' | 'material' (default: 'shadcn')
theme="base" // Optional: 'base' | 'brand' (default: 'base')
mode="system" // Optional: 'light' | 'dark' | 'system' (default: 'system')
storageKey="my-app-theme" // Optional: enables localStorage persistence
>
{/* Your application components */}
</ThemeProvider>
);
}Step 3: Use Components
Now you can use any component from the design system:
import { Button } from '@pd-design/system';
function MyComponent() {
return (
<Button>
Click me
</Button>
);
}That's it! 🎉 The ThemeProvider automatically handles:
- System preference detection - Follows OS dark/light mode
- localStorage persistence - Remembers user's theme choice (when
storageKeyis provided) - Cross-tab sync - Theme changes sync across browser tabs
- Class management - Automatically adds
pd-rootandpd-darkclasses - CSS variables - Sets all theme variables automatically
Implementation Guide
Complete Example: Using Button Component
Here's a complete example showing how to set up and use the Button component:
import { ThemeProvider, Button, useTheme } from '@pd-design/system';
import '@pd-design/system/styles.css';
function App() {
return (
<ThemeProvider>
<MyComponent />
</ThemeProvider>
);
}
function MyComponent() {
const { config, setConfig } = useTheme();
return (
<div>
{/* Basic Button */}
<Button variant="primary" size="md">
Primary Button
</Button>
{/* Button with Icons (must be valid Lucide icons - see https://lucide.dev/icons/) */}
<Button
variant="secondary"
size="lg"
startIcon="Download"
endIcon="ArrowRight"
>
Download File
</Button>
{/* Button States */}
<Button variant="primary" loading={true}>
Loading...
</Button>
<Button variant="primary" disabled={true}>
Disabled
</Button>
{/* Button Variants */}
<Button variant="primary">Primary</Button>
<Button variant="secondary">Secondary</Button>
<Button variant="ghost">Ghost</Button>
<Button variant="destructive">Delete</Button>
{/* Button Sizes */}
<Button size="sm">Small</Button>
<Button size="md">Medium</Button>
<Button size="lg">Large</Button>
{/* Theme Toggle Example */}
<Button onClick={() => {
const nextMode = config.mode === 'dark' ? 'light' : 'dark';
setConfig({ mode: nextMode });
}}>
Switch to {config.mode === 'dark' ? 'Light' : 'Dark'} Mode
</Button>
</div>
);
}Button Component API
The Button component accepts the following props:
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| variant | 'primary' \| 'secondary' \| 'ghost' \| 'destructive' | 'primary' | Visual style variant |
| size | 'sm' \| 'md' \| 'lg' | 'md' | Button size |
| loading | boolean | false | Shows loading state |
| disabled | boolean | false | Disables the button |
| fullWidth | boolean | false | Makes button full width |
| startIcon | string | - | Icon name to show before text (must be a valid Lucide icon) |
| endIcon | string | - | Icon name to show after text (must be a valid Lucide icon) |
| loadingText | string | - | Text to show during loading state |
| type | 'button' \| 'submit' \| 'reset' | 'button' | Button type for forms |
| href | string | - | Renders as link when provided |
| target | '_blank' \| '_self' \| '_parent' \| '_top' | undefined | Link target (when href is provided) |
| asChild | boolean | false | Render as child element (Radix UI pattern) |
Example Usage:
// Form submission button
<form onSubmit={handleSubmit}>
<Button type="submit" variant="primary">
Submit Form
</Button>
<Button type="reset" variant="secondary">
Reset
</Button>
</form>
// Button with icons (must be valid Lucide icons - see https://lucide.dev/icons/)
<Button startIcon="Download" variant="primary">
Download
</Button>
<Button endIcon="ArrowRight" variant="secondary">
Next Step
</Button>
// Loading state
<Button loading={isLoading} loadingText="Processing...">
Save Changes
</Button>
// Link button
<Button href="/dashboard" variant="ghost">
Go to Dashboard
</Button>Design System Architecture
The PD Design System follows a headless-first, adapter-based architecture:
┌─────────────────────────────────────────────────────────┐
│ Your Application │
│ (Uses components with props-only API, no className) │
└──────────────────────┬──────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ Design System Components │
│ (Button, Input, Select, etc. - Type-safe props API) │
└──────────────────────┬──────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ Adapter Layer │
│ (shadcn, Material, AntD, etc. - Switchable engines) │
└──────────────────────┬──────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ UI Engine Primitives │
│ (Radix UI, Material UI, Ant Design, etc.) │
└─────────────────────────────────────────────────────────┘Key Principles
- Props-only API: Consumers use only props and variants, no
classNamerequired - Full TypeScript: Complete autocomplete and type safety for all props
- Adapter Layer: Switch between UI engines without changing application code
- Semantic Tokens: Design tokens that work across themes and adapters
- Framework Agnostic: Design tokens and semantics are framework-agnostic
- Zero-Config Theming: Automatic system detection and localStorage sync
- Style Isolation: Scoped styles prevent conflicts with consumer CSS
Style Isolation
The design system uses scoped styling to prevent style conflicts:
- All Tailwind classes are prefixed with
pd-- No collision with consumer Tailwind - All CSS variables are scoped under
.pd-root- No token leakage - Preflight is disabled - Consumer styles are never reset
- Works with any consumer setup - Tailwind, MUI, AntD, Bootstrap, or plain CSS
Always wrap your components with ThemeProvider to create the scoped boundary:
<ThemeProvider storageKey="my-theme">
{/* Your design system components */}
</ThemeProvider>The ThemeProvider automatically adds the pd-root class to your body element, so all tokens are properly scoped.
Adapters
The adapter layer allows you to switch between different UI engines without changing your application code. This provides flexibility to use the underlying UI library that best fits your needs.
Supported Adapters
1. Shadcn Adapter (Default)
Built on Radix UI primitives and Tailwind CSS. This is the default adapter.
<ThemeProvider adapter="shadcn">
<Button variant="primary">Click me</Button>
</ThemeProvider>Features:
- Radix UI primitives for accessibility
- Tailwind CSS for styling
- Fully customizable via design tokens
- Zero runtime dependencies for styling
2. Material Adapter (Coming Soon)
Built on Material UI components.
<ThemeProvider adapter="material">
<Button variant="primary">Click me</Button>
</ThemeProvider>Note: Material adapter is planned for future releases. When available, you can switch adapters without changing your component code.
3. Ant Design Adapter (Planned)
Built on Ant Design components.
<ThemeProvider adapter="antd">
<Button variant="primary">Click me</Button>
</ThemeProvider>Note: Ant Design adapter is planned for future releases.
Switching Adapters
You can switch adapters at any time without changing your component code:
// Switch from shadcn to material (when available)
<ThemeProvider adapter="material" theme="base" mode="system">
<Button variant="primary">Same Button, Different Engine</Button>
</ThemeProvider>The component API remains the same regardless of the adapter used. This allows you to:
- Test different UI engines
- Migrate between engines gradually
- Use different engines for different parts of your application
- Choose the engine that best fits your performance and bundle size requirements
Adapter Configuration
By default, the shadcn adapter is used. You can optionally specify a different adapter:
// Default (no adapter prop needed)
<ThemeProvider>
<Button variant="primary">Click me</Button>
</ThemeProvider>
// Optional: Specify adapter explicitly
<ThemeProvider
adapter="shadcn" // Optional: 'shadcn' | 'material' | 'antd' (when available)
>
<Button variant="primary">Click me</Button>
</ThemeProvider>Default: 'shadcn'
Theming System
The design system supports themes and modes as separate concepts:
- Theme (
theme): The design theme name - defines brand colors and styling - Mode (
mode): The color scheme - light, dark, or system preference
Themes
Themes define the overall design language, including brand colors, typography, and styling preferences.
Base Theme (Default)
The base theme provides a neutral, professional design suitable for most applications. This is the default theme:
// Default (no theme prop needed)
<ThemeProvider>
<Button variant="primary">Base Theme Button</Button>
</ThemeProvider>Brand Theme (Optional)
The brand theme extends the base theme with custom brand colors:
// Optional: Use brand theme
<ThemeProvider theme="brand">
<Button variant="primary">Brand Theme Button</Button>
</ThemeProvider>Note: Brand theme customization is available for extending with your own brand colors.
Custom Theme Extension
You can extend themes with custom brand colors using CSS variables:
<ThemeProvider
theme="base"
mode="light"
className="pd-root [--pd-primary:220_80%_50%]"
>
{/* Custom primary color */}
<Button variant="primary">Custom Brand Color</Button>
</ThemeProvider>Or via CSS:
.pd-root {
--pd-primary: 220 80% 50%;
--pd-primary-foreground: 0 0% 98%;
/* Add more custom tokens as needed */
}Theme Configuration
By default, the base theme is used. You can optionally specify a different theme:
// Default (no theme prop needed)
<ThemeProvider>
<Button variant="primary">Base Theme</Button>
</ThemeProvider>
// Optional: Use brand theme
<ThemeProvider theme="brand">
<Button variant="primary">Brand Theme</Button>
</ThemeProvider>Default: 'base'
Mode System
The mode system controls the color scheme (light/dark) of your application. It supports three modes:
1. System Mode (Default)
Automatically follows the OS dark/light preference. This is the default mode:
// Default (no mode prop needed)
<ThemeProvider>
{/* Automatically follows OS preference */}
<Button variant="primary">System Mode</Button>
</ThemeProvider>Behavior:
- Detects OS preference on mount
- Updates automatically when OS preference changes
- Works seamlessly with localStorage persistence
- Recommended for most applications
2. Light Mode
Always uses light mode, ignoring system preference:
<ThemeProvider mode="light">
<Button variant="primary">Always Light</Button>
</ThemeProvider>3. Dark Mode
Always uses dark mode, ignoring system preference:
<ThemeProvider mode="dark">
<Button variant="primary">Always Dark</Button>
</ThemeProvider>Mode Configuration
By default, system mode is used. You can optionally specify a different mode:
// Default (no mode prop needed - follows OS preference)
<ThemeProvider>
<Button variant="primary">System Mode</Button>
</ThemeProvider>
// Optional: Force light mode
<ThemeProvider mode="light">
<Button variant="primary">Always Light</Button>
</ThemeProvider>
// Optional: Force dark mode
<ThemeProvider mode="dark">
<Button variant="primary">Always Dark</Button>
</ThemeProvider>Default: 'system'
localStorage Persistence
Pass storageKey to enable automatic theme persistence:
<ThemeProvider storageKey="my-app-theme">
{/*
Reads saved preference on mount
Saves changes automatically
Syncs across browser tabs
Works with system mode
*/}
</ThemeProvider>What gets saved:
- User's manual selection (
'light'|'dark') 'system'preference (so it knows to follow OS)
Example flow:
- User selects "Dark" → Saved to localStorage
- User refreshes page → Reads "Dark" from localStorage
- User opens new tab → Reads "Dark" from localStorage (synced)
- User changes to "System" → Saved, now follows OS preference
Theme Toggle Component
Here's an example of how to create a theme toggle:
import { useTheme, Button } from '@pd-design/system';
function ThemeToggle() {
const { config, setConfig } = useTheme();
const toggle = () => {
const modes = ['light', 'dark', 'system'] as const;
const currentIndex = modes.indexOf(config.mode);
const nextIndex = (currentIndex + 1) % modes.length;
setConfig({ mode: modes[nextIndex] });
};
return (
<Button onClick={toggle} variant="ghost">
{config.mode === 'system' && '🌓 System'}
{config.mode === 'light' && '☀️ Light'}
{config.mode === 'dark' && '🌙 Dark'}
</Button>
);
}Component Usage
Available Components
The design system provides a growing set of components. Here are some examples:
Button
import { Button } from '@pd-design/system';
<Button variant="primary" size="md">Click me</Button>
<Button variant="secondary" startIcon="Download">Download</Button>
<Button variant="destructive" loading={true}>Delete</Button>Browse all available icons: https://lucide.dev/icons/
Component Variants
All components follow a consistent variant system:
- variant:
primary|secondary|ghost|destructive - size:
sm|md|lg - state:
loading|disabled
No arbitrary strings or raw class overrides are allowed. This ensures consistency and type safety.
API Reference
ThemeProvider Props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| adapter | 'shadcn' \| 'material' | 'shadcn' | UI engine adapter |
| theme | 'base' \| 'brand' | 'base' | Theme name |
| mode | 'light' \| 'dark' \| 'system' | 'system' | Color mode (system follows OS) |
| storageKey | string? | undefined | localStorage key for persistence (enables sync) |
| children | ReactNode | Required | Your application components |
Exports
// Components
import {
Button,
Input,
Select,
Checkbox,
Radio,
Switch,
Modal,
Tooltip,
Dropdown,
Toast,
PdThemeProvider,
Icons
} from '@pd-design/system';
// Theme (Recommended: ThemeProvider with storageKey)
import {
ThemeProvider, // Full-featured provider with localStorage sync
useTheme, // Hook to access/update theme
setDesignSystemConfig,
getDesignSystemConfig
} from '@pd-design/system';
// Types
import type {
Variant,
Size,
ButtonType,
ThemeName,
ThemeMode,
AdapterType,
LucideIconName
} from '@pd-design/system';
// Styles (required)
import '@pd-design/system/styles.css';Advanced Usage
Per-Widget Theming
You can wrap only part of your page with PdThemeProvider for widget-level theming:
<div>
{/* Consumer styles */}
<PdThemeProvider theme="base" mode="dark">
{/* Design system widget with dark theme */}
<Button variant="primary">Dark Widget</Button>
</PdThemeProvider>
</div>Manual Configuration
For advanced use cases, you can configure the design system globally:
import { setDesignSystemConfig } from '@pd-design/system';
// Set adapter, theme, and mode
setDesignSystemConfig({
adapter: 'shadcn',
theme: 'base',
mode: 'light',
});Note: When using ThemeProvider with storageKey, you don't need to call setDesignSystemConfig manually - it's handled automatically.
PdThemeProvider vs ThemeProvider
PdThemeProvider: Lightweight wrapper that creates the.pd-rootscoped boundary. Use for simple theming needs without context.ThemeProvider: Full-featured provider with React context, system preference detection, localStorage sync, and CSS variable management. Recommended for most use cases.
Both support the same theme and mode props.
Common Use Cases
Use Case 1: Basic Setup (Default)
<ThemeProvider>
<App />
</ThemeProvider>What you get:
- Default adapter (
shadcn) - Default theme (
base) - System mode detection (follows OS)
- Zero configuration
Optional: Add Persistence
<ThemeProvider storageKey="my-app-theme">
<App />
</ThemeProvider>Additional benefits:
- Theme persistence (remembers user choice)
- Cross-tab sync
Use Case 2: Force Light Mode
<ThemeProvider mode="light">
<App />
</ThemeProvider>What you get:
- Always light mode
- Ignores system preference
- No localStorage (no storageKey)
Use Case 3: Custom Theme with System Mode
<ThemeProvider
theme="brand"
storageKey="my-brand-theme"
>
<App />
</ThemeProvider>What you get:
- Brand theme
- System mode with persistence
- User can override system preference
Use Case 4: Different Adapters
// Main app with shadcn
<ThemeProvider adapter="shadcn" storageKey="my-theme">
<MainApp />
</ThemeProvider>
// Widget with material (when available)
<PdThemeProvider adapter="material" theme="base" mode="dark">
<Widget />
</PdThemeProvider>Browser Support
- Chrome (latest)
- Firefox (latest)
- Safari (latest)
- Edge (latest)
- Mobile browsers (iOS Safari, Chrome Mobile)
Storybook
View all components and variants in Storybook:
npm run storybookStorybook includes:
- All component variants with live examples
- Interactive controls for all props
- Theme switcher (base/brand) - See components in different themes
- Mode switcher (light/dark/system) - Test system mode detection
- Adapter switcher (shadcn/material) - Switch UI engines
- Icons browser with search - Browse 1000+ Lucide icons
- Accessibility panel - WCAG compliance checks
- Code snippets - Copy-paste ready examples
- Design tokens showcase - See all color primitives and semantic tokens
TypeScript
Full TypeScript support with autocomplete for all props:
<Button
variant="primary" // Autocomplete: primary | secondary | ghost | destructive
size="md" // Autocomplete: sm | md | lg
startIcon="Download" // Autocomplete: All Lucide icon names
endIcon="ArrowRight" // Autocomplete: All Lucide icon names
/>
<ThemeProvider
theme="base" // Autocomplete: base | brand
mode="system" // Autocomplete: light | dark | system
adapter="shadcn" // Autocomplete: shadcn | material
/>Accessibility
- Radix UI primitives for ARIA compliance
- Keyboard navigation support (Tab, Enter, Space, Arrow keys)
- Focus ring standards (visible focus indicators)
- Screen reader labels (aria-label, aria-describedby)
- Contrast-safe tokens (WCAG AA compliant)
- Disabled state handling (aria-disabled, pointer-events)
- Loading state indicators (aria-busy)
Contributing
This is an internal design system. For contributions, please follow the architecture guidelines in ARCHITECTURE.md.
License
MIT
