Creating a Plugin
A ComposeKit plugin is a self-contained package under packages/@bplcp/<name>/ that exports a React component and a registration function. This guide walks through creating one end-to-end — from the package skeleton to wiring it into the playground and gallery.
/create-plugin Claude command to scaffold everything automatically. Pass name [Title] [description] [group] [version] as arguments and it will generate all files, wire up all configs, and run the checklist for you.Step 0 — Derive names & check shadcn
From the kebab-case name (e.g. date-picker) derive the following identifiers used throughout:
Title— PascalCase, e.g.DatePickerComponentName—ComposeKit{Title}, e.g.ComposeKitDatePickercamel— camelCase, e.g.datePickerversion— read frompackages/@bplcp/composekit/package.json
Check whether a shadcn primitive exists at packages/@bplcp/ds/shadcn/components/ui/<name>.tsx. If it does, copy it (and utils.ts) to src/<name>/shadcn/ and never edit those copies. Otherwise, build with plain Tailwind classes.
Also scan 2–3 existing similar plugins for patterns before writing any code:packages/@bplcp/tabs/src/ (structural), packages/@bplcp/stepper/src/ (no shadcn),packages/@bplcp/input/src/ (shadcn-wrapped form).
Step 1 — Create the package
Create packages/@bplcp/<name>/ with this structure:
packages/@bplcp/<name>/ ├── src/ │ ├── index.ts │ └── <name>/ │ ├── <name>.component.tsx ← main React component │ ├── <name>.plugin.ts ← plugin registration │ ├── <name>.types.ts ← ISomething interface + prop types │ └── shadcn/ ← (only if shadcn primitive exists) ├── package.json ├── tsconfig.json ├── webpack.config.js ├── babel.config.js ├── postcss.config.js ├── tailwind.config.js ├── schema.json └── docs.md
Copy verbatim from packages/@bplcp/stepper/ — only change name and version in package.json.
// packages/@bplcp/<name>/package.json
// Copy from packages/@bplcp/stepper/package.json — same scripts, same devDependencies.
// Only change: name and version.
{
"name": "@bplcp/<name>",
"version": "<version>"
}// tailwind.config.js
module.exports = {
content: ['./src/**/*.{ts,tsx}'],
corePlugins: { preflight: false }, // ← always disable preflight in component packages
theme: { extend: {} },
plugins: []
};// src/index.ts
export { ComposeKit<Title> } from './<name>/<name>.component';
export { <name>Plugin } from './<name>/<name>.plugin';// src/<name>/<name>.types.ts
import type { CSSProperties } from 'react';
export interface IWorkflow {
workflowRef: string;
initialVars?: { key: string; value: string }[];
}
export interface I<Title> {
// component-specific props here
className?: string;
style?: CSSProperties;
events?: {
onChange?: { workflows?: IWorkflow[] };
};
onChange?: (value: any) => void;
onMount?: (el: HTMLElement) => void;
onUnmount?: () => void;
}// src/<name>/<name>.component.tsx
import React, { useEffect, useRef } from 'react';
import { workflowService } from '@bplcp/core-services';
import type { I<Title> } from './<name>.types';
export const ComposeKit<Title> = React.memo(
React.forwardRef<HTMLDivElement, I<Title>>(({
className = '',
style = {},
events,
onChange,
onMount,
onUnmount,
}, forwardedRef) => {
const innerRef = useRef<HTMLDivElement>(null);
const resolvedRef = (forwardedRef as React.RefObject<HTMLDivElement>) ?? innerRef;
useEffect(() => {
if (resolvedRef.current) onMount?.(resolvedRef.current);
return () => { onUnmount?.(); };
}, []);
const handleChange = (value: any) => {
onChange?.(value);
const workflows = events?.onChange?.workflows;
workflows?.length && workflowService.execute(workflows);
};
return (
<div ref={resolvedRef} className={className} style={style}>
{/* component JSX */}
</div>
);
})
);
export default ComposeKit<Title>;// src/<name>/<name>.plugin.ts
import { CoreServices } from '@bplcp/core-services';
export function <name>Plugin(services: CoreServices) {
services.pluginRegistry.register({
name: '@bplcp/<name>',
component: () => import('./<name>.component').then(m => m.ComposeKit<Title>),
schema: () => import('../../schema.json'),
});
}// If a shadcn primitive exists at:
// packages/@bplcp/ds/shadcn/components/ui/<name>.tsx
//
// Copy it (and utils.ts) to:
// src/<name>/shadcn/<name>.tsx
// src/<name>/shadcn/utils.ts ← use the @bplcp/core-services version
//
// utils.ts always looks like this:
import { utilsService } from '@bplcp/core-services';
export function cn(...inputs: any[]) {
return utilsService.twMerge(inputs);
}
// Never edit files inside shadcn/ — they are copies of the upstream shadcn component.Always use CSS custom properties via inline styles. Tailwind classes are fine for layout and spacing only.
// ✅ Correct — use CSS custom properties directly
<div style={{ color: 'var(--primary)', background: 'var(--muted)' }}>
// ❌ Wrong — do not use hsl() wrapper (project uses oklch Tailwind v4 variables)
<div style={{ color: 'hsl(var(--primary))' }}>
// ❌ Wrong — do not use Tailwind color classes for component colors
<div className="text-primary bg-muted">Generate a complete JSON Schema for every prop. The "default" object must be a concrete example that renders without errors when the component is dropped onto the canvas. Omit onMount, onUnmount, and style from props — they are internal or too complex for the schema editor.
// schema.json — minimal example
{
"$schema": "http://json-schema.org",
"title": "<Title> Configuration",
"description": "<description>",
"type": "object",
"default": {
// ← concrete example — must render without errors on drag-and-drop
},
"required": [],
"properties": {
"className": { "title": "CSS Class", "type": "string" },
"style": {
"title": "Custom Styles",
"type": "object",
"additionalProperties": { "type": "string" }
}
// ... component-specific props
}
}Step 2 — Wire into composekit package
// packages/@bplcp/composekit/src/plugins/<name>.ts
export { <name>Plugin as default } from '@bplcp/<name>';// packages/@bplcp/composekit/webpack.config.js
// 1. Add to PLUGIN_ENTRIES array:
{ subpath: '<name>', entry: './src/plugins/<name>.ts' },
// 2. Add to alias block:
'@bplcp/<name>': path.resolve(__dirname, '../<name>/src/index.ts'),// packages/@bplcp/composekit/package.json
// Add to "exports":
"./<name>": {
"types": "./dist/types/plugins/<name>.d.ts",
"default": "./dist/plugins/<name>.js"
}
// Add to "typesVersions["*"]":
"<name>": ["dist/types/plugins/<name>.d.ts"]// packages/@bplcp/composekit/tsconfig.json — add to "paths": "@bplcp/<name>": ["../<name>/src/index.ts"]
Step 3 — Wire into portal (vite.config.ts)
// portal/composekit/vite.config.ts // Add to resolve.alias: '@brainpiper/composekit/<name>': path.resolve(pkgs, 'composekit/src/plugins/<name>.ts'), '@bplcp/<name>': path.resolve(pkgs, '<name>/src/index.ts'), // Add to optimizeDeps.exclude: '@brainpiper/composekit/<name>',
Add the component to playground.scope.ts so the AI playground can use it in generated code:
// portal/composekit/src/playground/playground.scope.ts
import { ComposeKit<Title> } from '@brainpiper/composekit/<name>';
export const PLAYGROUND_SCOPE = {
// ... existing components
ComposeKit<Title>,
};Step 4 — Docs portal
Add a lazy import and entry to ComponentsSection.tsx, a changelog entry to OverviewSection.tsx, and update the component count and group tags in App.tsx. See existing entries (e.g. stepper) as a reference for placement.
Step 5 — Gallery widget
Create an interactive widget under portal/composekit/src/gallery/widgets/<Title>Widget.tsxand register it as a lazy import in IndividualComponents.tsx. The widget should show 2–3 variants with real, meaningful example data — not lorem ipsum.
// portal/composekit/src/gallery/widgets/<Title>Widget.tsx
import React from 'react';
import { ComposeKit<Title> } from '@brainpiper/composekit/<name>';
export default function <Title>Widget() {
return (
<div className="widget-card">
<div className="widget-card-header">
<h3><Title></h3>
<p>Short description of the component.</p>
</div>
<div className="widget-card-body">
{/* Show 2–3 variants with real, meaningful data */}
<ComposeKit<Title> {/* props */} />
</div>
</div>
);
}Final checklist
- No test artifacts left anywhere
- No duplicate lines inserted in modified files
schema.jsonhas a"default"object that renders on canvas drag-and-drop- Component uses
var(--primary)nothsl(var(--primary))for colors - Shadcn files (if any) are in
src/<name>/shadcn/and are not edited - All config files updated: composekit webpack, package.json, tsconfig, vite.config
docs.mdexists with a props table and 2–3 usage examples