feat(marketplace): command consolidation + 8 new plugins (v8.1.0 → v9.0.0) [BREAKING]
Phase 1b: Rename all ~94 commands across 12 plugins to /<noun> <action> sub-command pattern. Git-flow consolidated from 8→5 commands (commit variants absorbed into --push/--merge/--sync flags). Dispatch files, name: frontmatter, and cross-reference updates for all plugins. Phase 2: Design documents for 8 new plugins in docs/designs/. Phase 3: Scaffold 8 new plugins — saas-api-platform, saas-db-migrate, saas-react-platform, saas-test-pilot, data-seed, ops-release-manager, ops-deploy-pipeline, debug-mcp. Each with plugin.json, commands, agents, skills, README, and claude-md-integration. Marketplace grows from 12→20. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
134
plugins/saas-react-platform/skills/component-patterns.md
Normal file
134
plugins/saas-react-platform/skills/component-patterns.md
Normal file
@@ -0,0 +1,134 @@
|
||||
---
|
||||
name: component-patterns
|
||||
description: Component structure conventions including functional components, prop typing, exports, and co-located tests
|
||||
---
|
||||
|
||||
# Component Patterns
|
||||
|
||||
## Purpose
|
||||
|
||||
Define standard patterns for React component scaffolding. This skill ensures all generated components follow consistent structure, typing, export conventions, and test co-location.
|
||||
|
||||
---
|
||||
|
||||
## Component File Structure
|
||||
|
||||
Every component file follows this order:
|
||||
|
||||
```typescript
|
||||
// 1. Imports (external first, then internal, then styles)
|
||||
import { type FC } from 'react';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import styles from './ComponentName.module.css';
|
||||
|
||||
// 2. Types (inline for simple, separate file for complex)
|
||||
interface ComponentNameProps {
|
||||
title: string;
|
||||
onAction: () => void;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
// 3. Component definition
|
||||
/**
|
||||
* Brief description of what this component does.
|
||||
*
|
||||
* @component
|
||||
* @example
|
||||
* <ComponentName title="Hello" onAction={() => console.log('clicked')} />
|
||||
*/
|
||||
const ComponentName: FC<ComponentNameProps> = ({ title, onAction, children }) => {
|
||||
return (
|
||||
<div>
|
||||
<h2>{title}</h2>
|
||||
{children}
|
||||
<button onClick={onAction}>Action</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// 4. Display name (for DevTools)
|
||||
ComponentName.displayName = 'ComponentName';
|
||||
|
||||
// 5. Export
|
||||
export default ComponentName;
|
||||
```
|
||||
|
||||
## Component Type Templates
|
||||
|
||||
### UI Component (presentational)
|
||||
- Props in, JSX out — no side effects, no data fetching
|
||||
- Pure function: same props always produce same output
|
||||
- Accept `className` prop for style override flexibility
|
||||
- Accept `children` if component is a container/wrapper
|
||||
|
||||
### Page Component
|
||||
- Includes data fetching (server component in App Router, `useEffect` in client)
|
||||
- Loading state with skeleton placeholder
|
||||
- Error state with retry action
|
||||
- `'use client'` directive only if client interactivity required (App Router)
|
||||
|
||||
### Layout Component
|
||||
- Accepts `children: React.ReactNode` as required prop
|
||||
- Optional slot props for sidebar, header, footer
|
||||
- Handles responsive behavior
|
||||
- Wraps with error boundary
|
||||
|
||||
### Form Component
|
||||
- Controlled inputs with `useState` or form library (`react-hook-form`)
|
||||
- Typed form values interface
|
||||
- Validation schema (Zod recommended)
|
||||
- Submit handler with loading state
|
||||
- Error display per field and form-level
|
||||
|
||||
## Test Co-location Patterns
|
||||
|
||||
Test file sits next to component file:
|
||||
|
||||
```
|
||||
src/components/Button/
|
||||
Button.tsx
|
||||
Button.test.tsx
|
||||
Button.module.css (if CSS Modules)
|
||||
index.ts (barrel file)
|
||||
```
|
||||
|
||||
### Minimum Test Coverage
|
||||
|
||||
```typescript
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import ComponentName from './ComponentName';
|
||||
|
||||
describe('ComponentName', () => {
|
||||
it('renders without crashing', () => {
|
||||
render(<ComponentName title="Test" onAction={() => {}} />);
|
||||
expect(screen.getByText('Test')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('calls onAction when button clicked', async () => {
|
||||
const onAction = vi.fn();
|
||||
render(<ComponentName title="Test" onAction={onAction} />);
|
||||
await userEvent.click(screen.getByRole('button'));
|
||||
expect(onAction).toHaveBeenCalledOnce();
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Barrel File Convention
|
||||
|
||||
Each component directory exports through `index.ts`:
|
||||
|
||||
```typescript
|
||||
export { default as ComponentName } from './ComponentName';
|
||||
export type { ComponentNameProps } from './ComponentName';
|
||||
```
|
||||
|
||||
## Anti-Patterns to Avoid
|
||||
|
||||
| Pattern | Why | Alternative |
|
||||
|---------|-----|-------------|
|
||||
| Class components | Legacy API, verbose | Functional components + hooks |
|
||||
| `React.FC` with children | Children always optional, incorrect type narrowing | Explicit `children` prop in interface |
|
||||
| Prop spreading `{...props}` | Obscures expected interface | Explicitly destructure needed props |
|
||||
| `useEffect` for derived state | Unnecessary render cycle | Compute during render or `useMemo` |
|
||||
| `forwardRef` without `displayName` | Unnamed in DevTools | Always set `displayName` |
|
||||
96
plugins/saas-react-platform/skills/framework-detection.md
Normal file
96
plugins/saas-react-platform/skills/framework-detection.md
Normal file
@@ -0,0 +1,96 @@
|
||||
---
|
||||
name: framework-detection
|
||||
description: Detect React framework, TypeScript configuration, routing structure, and project conventions
|
||||
---
|
||||
|
||||
# Framework Detection
|
||||
|
||||
## Purpose
|
||||
|
||||
Analyze a project's `package.json`, configuration files, and directory structure to determine the React framework, TypeScript usage, routing pattern, and CSS approach. This skill is loaded at the start of setup and routing commands to adapt output to the project's specific toolchain.
|
||||
|
||||
---
|
||||
|
||||
## Framework Detection Rules
|
||||
|
||||
Check `package.json` dependencies and devDependencies in this order (first match wins):
|
||||
|
||||
| Framework | Detection Criteria | Routing |
|
||||
|-----------|-------------------|---------|
|
||||
| **Next.js (App Router)** | `next` in deps + `app/` directory exists | File-based (`app/page.tsx`) |
|
||||
| **Next.js (Pages Router)** | `next` in deps + `pages/` directory exists (no `app/`) | File-based (`pages/index.tsx`) |
|
||||
| **Remix** | `@remix-run/react` in deps | File-based with loaders (`routes/`) |
|
||||
| **Vite + React** | `vite` in deps + `@vitejs/plugin-react` | Client-side (react-router or tanstack-router) |
|
||||
| **Create React App** | `react-scripts` in deps | Client-side (react-router) |
|
||||
| **Gatsby** | `gatsby` in deps | File-based (`src/pages/`) |
|
||||
|
||||
## TypeScript Detection
|
||||
|
||||
| Signal | Conclusion |
|
||||
|--------|------------|
|
||||
| `tsconfig.json` exists | TypeScript project |
|
||||
| `typescript` in devDependencies | TypeScript project |
|
||||
| `.tsx` files in `src/` or `app/` | TypeScript project |
|
||||
| None of the above | JavaScript project — generate `.jsx` files |
|
||||
|
||||
## CSS Approach Detection
|
||||
|
||||
Check in order:
|
||||
|
||||
| Signal | Approach |
|
||||
|--------|----------|
|
||||
| `tailwindcss` in deps + `tailwind.config.*` | Tailwind CSS |
|
||||
| `*.module.css` or `*.module.scss` files exist | CSS Modules |
|
||||
| `styled-components` in deps | styled-components |
|
||||
| `@emotion/react` in deps | Emotion |
|
||||
| `vanilla-extract` in deps | Vanilla Extract |
|
||||
| None detected | Plain CSS or inline styles |
|
||||
|
||||
## Test Runner Detection
|
||||
|
||||
| Signal | Runner |
|
||||
|--------|--------|
|
||||
| `vitest` in devDependencies | Vitest |
|
||||
| `jest` in devDependencies or `jest.config.*` exists | Jest |
|
||||
| `@testing-library/react` in deps | Testing Library (works with both) |
|
||||
| `cypress` in deps | Cypress (E2E, not unit) |
|
||||
|
||||
## State Management Detection
|
||||
|
||||
| Signal | Library |
|
||||
|--------|---------|
|
||||
| `zustand` in dependencies | Zustand |
|
||||
| `@reduxjs/toolkit` in dependencies | Redux Toolkit |
|
||||
| `recoil` in dependencies | Recoil |
|
||||
| `jotai` in dependencies | Jotai |
|
||||
| `mobx-react` in dependencies | MobX |
|
||||
| Files with `createContext` + `useReducer` pattern | React Context (built-in) |
|
||||
|
||||
## Directory Structure Patterns
|
||||
|
||||
Common patterns to detect and respect:
|
||||
|
||||
| Pattern | Typical Path | Detection |
|
||||
|---------|-------------|-----------|
|
||||
| Feature-based | `src/features/<feature>/components/` | `features/` directory with subdirectories |
|
||||
| Component-based | `src/components/<Component>/` | `components/` with PascalCase subdirectories |
|
||||
| Flat components | `src/components/*.tsx` | `components/` with files only, no subdirectories |
|
||||
| Atomic design | `src/components/atoms/`, `molecules/`, `organisms/` | Atomic naming directories |
|
||||
|
||||
## Output
|
||||
|
||||
Store detected configuration as a reference object for other skills:
|
||||
|
||||
```json
|
||||
{
|
||||
"framework": "nextjs-app",
|
||||
"typescript": true,
|
||||
"css_approach": "tailwind",
|
||||
"test_runner": "vitest",
|
||||
"state_management": "zustand",
|
||||
"component_dir": "src/components",
|
||||
"pages_dir": "app",
|
||||
"hooks_dir": "src/hooks",
|
||||
"structure_pattern": "feature-based"
|
||||
}
|
||||
```
|
||||
174
plugins/saas-react-platform/skills/routing-conventions.md
Normal file
174
plugins/saas-react-platform/skills/routing-conventions.md
Normal file
@@ -0,0 +1,174 @@
|
||||
---
|
||||
name: routing-conventions
|
||||
description: File-based routing (Next.js), react-router conventions, dynamic routes, layouts, and middleware
|
||||
---
|
||||
|
||||
# Routing Conventions
|
||||
|
||||
## Purpose
|
||||
|
||||
Define routing patterns for each supported framework. This skill ensures route scaffolding produces the correct file structure, naming conventions, and framework-specific boilerplate.
|
||||
|
||||
---
|
||||
|
||||
## Next.js App Router (v13.4+)
|
||||
|
||||
### File Conventions
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `page.tsx` | Route UI — required to make segment publicly accessible |
|
||||
| `layout.tsx` | Shared layout wrapping child pages — persists across navigations |
|
||||
| `loading.tsx` | Loading UI shown while page is loading (Suspense boundary) |
|
||||
| `error.tsx` | Error UI shown when page throws (must be client component) |
|
||||
| `not-found.tsx` | 404 UI for segment |
|
||||
| `route.ts` | API route handler (GET, POST, etc.) |
|
||||
|
||||
### Route Patterns
|
||||
|
||||
```
|
||||
app/
|
||||
page.tsx # /
|
||||
about/page.tsx # /about
|
||||
blog/page.tsx # /blog
|
||||
blog/[slug]/page.tsx # /blog/:slug (dynamic)
|
||||
dashboard/
|
||||
layout.tsx # Shared dashboard layout
|
||||
page.tsx # /dashboard
|
||||
settings/page.tsx # /dashboard/settings
|
||||
(marketing)/ # Route group (no URL segment)
|
||||
pricing/page.tsx # /pricing
|
||||
```
|
||||
|
||||
### Dynamic Routes
|
||||
|
||||
| Pattern | File Path | URL Match |
|
||||
|---------|-----------|-----------|
|
||||
| Dynamic segment | `[id]/page.tsx` | `/users/123` |
|
||||
| Catch-all | `[...slug]/page.tsx` | `/docs/a/b/c` |
|
||||
| Optional catch-all | `[[...slug]]/page.tsx` | `/docs` or `/docs/a/b` |
|
||||
|
||||
### Server vs Client Components
|
||||
|
||||
- Pages are Server Components by default
|
||||
- Add `'use client'` directive only when using: `useState`, `useEffect`, `onClick`, browser APIs
|
||||
- Pass data from server to client via props, not through context
|
||||
|
||||
## Next.js Pages Router (Legacy)
|
||||
|
||||
### File Conventions
|
||||
|
||||
```
|
||||
pages/
|
||||
index.tsx # /
|
||||
about.tsx # /about
|
||||
blog/index.tsx # /blog
|
||||
blog/[slug].tsx # /blog/:slug
|
||||
_app.tsx # App wrapper (layouts)
|
||||
_document.tsx # HTML document customization
|
||||
404.tsx # Custom 404 page
|
||||
api/users.ts # API route: /api/users
|
||||
```
|
||||
|
||||
### Data Fetching
|
||||
|
||||
| Method | When | Use Case |
|
||||
|--------|------|----------|
|
||||
| `getServerSideProps` | Every request | Dynamic data, auth-gated pages |
|
||||
| `getStaticProps` | Build time | Blog posts, marketing pages |
|
||||
| `getStaticPaths` | Build time | Dynamic routes with static generation |
|
||||
|
||||
## React Router (v6+)
|
||||
|
||||
### Route Definition
|
||||
|
||||
```typescript
|
||||
// router.tsx
|
||||
import { createBrowserRouter, RouterProvider } from 'react-router-dom';
|
||||
import { lazy, Suspense } from 'react';
|
||||
|
||||
const Dashboard = lazy(() => import('./pages/Dashboard'));
|
||||
const Settings = lazy(() => import('./pages/Settings'));
|
||||
|
||||
const router = createBrowserRouter([
|
||||
{
|
||||
path: '/',
|
||||
element: <RootLayout />,
|
||||
errorElement: <ErrorBoundary />,
|
||||
children: [
|
||||
{ index: true, element: <Home /> },
|
||||
{
|
||||
path: 'dashboard',
|
||||
element: <Suspense fallback={<Loading />}><Dashboard /></Suspense>,
|
||||
},
|
||||
{
|
||||
path: 'users/:id',
|
||||
element: <UserProfile />,
|
||||
loader: userLoader,
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
```
|
||||
|
||||
### Layout Pattern
|
||||
|
||||
```typescript
|
||||
// layouts/RootLayout.tsx
|
||||
import { Outlet } from 'react-router-dom';
|
||||
|
||||
export function RootLayout() {
|
||||
return (
|
||||
<div>
|
||||
<Header />
|
||||
<main>
|
||||
<Outlet />
|
||||
</main>
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Protected Routes
|
||||
|
||||
### Pattern: Auth Guard Component
|
||||
|
||||
```typescript
|
||||
function ProtectedRoute({ children }: { children: React.ReactNode }) {
|
||||
const { isAuthenticated, isLoading } = useAuth();
|
||||
|
||||
if (isLoading) return <LoadingSkeleton />;
|
||||
if (!isAuthenticated) return <Navigate to="/login" replace />;
|
||||
|
||||
return <>{children}</>;
|
||||
}
|
||||
```
|
||||
|
||||
### App Router: Middleware
|
||||
|
||||
```typescript
|
||||
// middleware.ts (project root)
|
||||
import { NextResponse } from 'next/server';
|
||||
import type { NextRequest } from 'next/server';
|
||||
|
||||
export function middleware(request: NextRequest) {
|
||||
const token = request.cookies.get('session');
|
||||
if (!token && request.nextUrl.pathname.startsWith('/dashboard')) {
|
||||
return NextResponse.redirect(new URL('/login', request.url));
|
||||
}
|
||||
return NextResponse.next();
|
||||
}
|
||||
|
||||
export const config = { matcher: ['/dashboard/:path*'] };
|
||||
```
|
||||
|
||||
## Error Boundaries
|
||||
|
||||
Every page route should have an error boundary:
|
||||
|
||||
- App Router: `error.tsx` file in route segment (automatically client component)
|
||||
- React Router: `errorElement` prop on route definition
|
||||
- Fallback: Generic `ErrorBoundary` component wrapping page content
|
||||
|
||||
Include retry functionality and user-friendly error message. Log error details to console (placeholder for error reporting service).
|
||||
203
plugins/saas-react-platform/skills/state-patterns.md
Normal file
203
plugins/saas-react-platform/skills/state-patterns.md
Normal file
@@ -0,0 +1,203 @@
|
||||
---
|
||||
name: state-patterns
|
||||
description: State management patterns — React Context for simple, Zustand for medium, Redux Toolkit for complex
|
||||
---
|
||||
|
||||
# State Management Patterns
|
||||
|
||||
## Purpose
|
||||
|
||||
Guide state management decisions and provide scaffolding templates for React Context, Zustand, and Redux Toolkit. This skill helps select the right pattern based on complexity and generates consistent store implementations.
|
||||
|
||||
---
|
||||
|
||||
## Decision Framework
|
||||
|
||||
| Criteria | Context | Zustand | Redux Toolkit |
|
||||
|----------|---------|---------|---------------|
|
||||
| **Scope** | Single feature, few consumers | Multiple features, medium consumers | App-wide, many consumers |
|
||||
| **Complexity** | Simple values (theme, locale, auth) | Medium (cart, form wizard, filters) | Complex (normalized entities, async workflows) |
|
||||
| **Async logic** | Manual with `useEffect` | Built-in with async actions | `createAsyncThunk` with lifecycle |
|
||||
| **DevTools** | None built-in | Optional middleware | Full Redux DevTools integration |
|
||||
| **Dependencies** | None (built-in React) | ~2KB, zero config | ~12KB, more boilerplate |
|
||||
| **Learning curve** | Low | Low | Medium-High |
|
||||
|
||||
### Quick Decision
|
||||
|
||||
- Need to share a simple value across a few components? **Context**
|
||||
- Need a store with some async logic and moderate complexity? **Zustand**
|
||||
- Need normalized state, middleware, complex async flows, or strict patterns? **Redux Toolkit**
|
||||
|
||||
## React Context Template
|
||||
|
||||
```typescript
|
||||
// stores/auth-context.tsx
|
||||
import { createContext, useContext, useReducer, type ReactNode } from 'react';
|
||||
|
||||
// State type
|
||||
interface AuthState {
|
||||
user: User | null;
|
||||
isAuthenticated: boolean;
|
||||
isLoading: boolean;
|
||||
}
|
||||
|
||||
// Action types
|
||||
type AuthAction =
|
||||
| { type: 'LOGIN'; payload: User }
|
||||
| { type: 'LOGOUT' }
|
||||
| { type: 'SET_LOADING'; payload: boolean };
|
||||
|
||||
// Initial state
|
||||
const initialState: AuthState = {
|
||||
user: null,
|
||||
isAuthenticated: false,
|
||||
isLoading: false,
|
||||
};
|
||||
|
||||
// Reducer
|
||||
function authReducer(state: AuthState, action: AuthAction): AuthState {
|
||||
switch (action.type) {
|
||||
case 'LOGIN':
|
||||
return { ...state, user: action.payload, isAuthenticated: true, isLoading: false };
|
||||
case 'LOGOUT':
|
||||
return { ...state, user: null, isAuthenticated: false };
|
||||
case 'SET_LOADING':
|
||||
return { ...state, isLoading: action.payload };
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
// Context
|
||||
const AuthContext = createContext<{
|
||||
state: AuthState;
|
||||
dispatch: React.Dispatch<AuthAction>;
|
||||
} | null>(null);
|
||||
|
||||
// Provider
|
||||
export function AuthProvider({ children }: { children: ReactNode }) {
|
||||
const [state, dispatch] = useReducer(authReducer, initialState);
|
||||
return (
|
||||
<AuthContext.Provider value={{ state, dispatch }}>
|
||||
{children}
|
||||
</AuthContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
// Hook with validation
|
||||
export function useAuth() {
|
||||
const context = useContext(AuthContext);
|
||||
if (!context) {
|
||||
throw new Error('useAuth must be used within AuthProvider');
|
||||
}
|
||||
return context;
|
||||
}
|
||||
```
|
||||
|
||||
## Zustand Template
|
||||
|
||||
```typescript
|
||||
// stores/cart-store.ts
|
||||
import { create } from 'zustand';
|
||||
import { devtools, persist } from 'zustand/middleware';
|
||||
|
||||
interface CartItem {
|
||||
id: string;
|
||||
name: string;
|
||||
price: number;
|
||||
quantity: number;
|
||||
}
|
||||
|
||||
interface CartState {
|
||||
items: CartItem[];
|
||||
addItem: (item: Omit<CartItem, 'quantity'>) => void;
|
||||
removeItem: (id: string) => void;
|
||||
clearCart: () => void;
|
||||
totalPrice: () => number;
|
||||
}
|
||||
|
||||
export const useCartStore = create<CartState>()(
|
||||
devtools(
|
||||
persist(
|
||||
(set, get) => ({
|
||||
items: [],
|
||||
addItem: (item) => set((state) => {
|
||||
const existing = state.items.find((i) => i.id === item.id);
|
||||
if (existing) {
|
||||
return { items: state.items.map((i) =>
|
||||
i.id === item.id ? { ...i, quantity: i.quantity + 1 } : i
|
||||
)};
|
||||
}
|
||||
return { items: [...state.items, { ...item, quantity: 1 }] };
|
||||
}),
|
||||
removeItem: (id) => set((state) => ({
|
||||
items: state.items.filter((i) => i.id !== id),
|
||||
})),
|
||||
clearCart: () => set({ items: [] }),
|
||||
totalPrice: () => get().items.reduce(
|
||||
(sum, item) => sum + item.price * item.quantity, 0
|
||||
),
|
||||
}),
|
||||
{ name: 'cart-storage' }
|
||||
)
|
||||
)
|
||||
);
|
||||
```
|
||||
|
||||
## Redux Toolkit Template
|
||||
|
||||
```typescript
|
||||
// store/slices/productsSlice.ts
|
||||
import { createSlice, createAsyncThunk, type PayloadAction } from '@reduxjs/toolkit';
|
||||
|
||||
// Async thunk
|
||||
export const fetchProducts = createAsyncThunk(
|
||||
'products/fetchAll',
|
||||
async (_, { rejectWithValue }) => {
|
||||
try {
|
||||
const response = await fetch('/api/products');
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
return rejectWithValue('Failed to fetch products');
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Slice
|
||||
const productsSlice = createSlice({
|
||||
name: 'products',
|
||||
initialState: {
|
||||
items: [] as Product[],
|
||||
status: 'idle' as 'idle' | 'loading' | 'succeeded' | 'failed',
|
||||
error: null as string | null,
|
||||
},
|
||||
reducers: {
|
||||
updateProduct: (state, action: PayloadAction<Product>) => {
|
||||
const index = state.items.findIndex((p) => p.id === action.payload.id);
|
||||
if (index !== -1) state.items[index] = action.payload;
|
||||
},
|
||||
},
|
||||
extraReducers: (builder) => {
|
||||
builder
|
||||
.addCase(fetchProducts.pending, (state) => { state.status = 'loading'; })
|
||||
.addCase(fetchProducts.fulfilled, (state, action) => {
|
||||
state.status = 'succeeded';
|
||||
state.items = action.payload;
|
||||
})
|
||||
.addCase(fetchProducts.rejected, (state, action) => {
|
||||
state.status = 'failed';
|
||||
state.error = action.payload as string;
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export const { updateProduct } = productsSlice.actions;
|
||||
export default productsSlice.reducer;
|
||||
```
|
||||
|
||||
## When NOT to Use Global State
|
||||
|
||||
- Form input values (use local `useState` or `react-hook-form`)
|
||||
- UI toggle state (modal open/close) unless shared across routes
|
||||
- Computed values derivable from existing state (compute inline or `useMemo`)
|
||||
- Server cache data (use TanStack Query or SWR instead of Redux)
|
||||
137
plugins/saas-react-platform/skills/typescript-patterns.md
Normal file
137
plugins/saas-react-platform/skills/typescript-patterns.md
Normal file
@@ -0,0 +1,137 @@
|
||||
---
|
||||
name: typescript-patterns
|
||||
description: Utility types, generics for components, discriminated unions for props, and strict null checks
|
||||
---
|
||||
|
||||
# TypeScript Patterns for React
|
||||
|
||||
## Purpose
|
||||
|
||||
Define TypeScript patterns specific to React component development. This skill ensures generated code uses idiomatic TypeScript with proper generic constraints, discriminated unions, and utility types.
|
||||
|
||||
---
|
||||
|
||||
## Props Interface Conventions
|
||||
|
||||
### Basic Props
|
||||
```typescript
|
||||
interface ButtonProps {
|
||||
label: string;
|
||||
onClick: () => void;
|
||||
variant?: 'primary' | 'secondary' | 'ghost';
|
||||
disabled?: boolean;
|
||||
className?: string;
|
||||
}
|
||||
```
|
||||
|
||||
### Props with Children
|
||||
```typescript
|
||||
interface CardProps {
|
||||
title: string;
|
||||
children: React.ReactNode; // Explicit, not via FC
|
||||
}
|
||||
```
|
||||
|
||||
### Discriminated Union Props
|
||||
Use when a component has mutually exclusive modes:
|
||||
```typescript
|
||||
type AlertProps =
|
||||
| { variant: 'success'; message: string }
|
||||
| { variant: 'error'; message: string; retry: () => void }
|
||||
| { variant: 'loading'; progress?: number };
|
||||
```
|
||||
|
||||
### Extending HTML Element Props
|
||||
```typescript
|
||||
interface InputProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'size'> {
|
||||
label: string;
|
||||
error?: string;
|
||||
size?: 'sm' | 'md' | 'lg'; // Custom size, not HTML size
|
||||
}
|
||||
```
|
||||
|
||||
## Generic Component Patterns
|
||||
|
||||
### Generic List Component
|
||||
```typescript
|
||||
interface ListProps<T> {
|
||||
items: T[];
|
||||
renderItem: (item: T, index: number) => React.ReactNode;
|
||||
keyExtractor: (item: T) => string;
|
||||
emptyMessage?: string;
|
||||
}
|
||||
|
||||
function List<T>({ items, renderItem, keyExtractor, emptyMessage }: ListProps<T>) {
|
||||
if (items.length === 0) return <p>{emptyMessage ?? 'No items'}</p>;
|
||||
return <ul>{items.map((item, i) => <li key={keyExtractor(item)}>{renderItem(item, i)}</li>)}</ul>;
|
||||
}
|
||||
```
|
||||
|
||||
### Generic Hook
|
||||
```typescript
|
||||
function useLocalStorage<T>(key: string, initialValue: T): [T, (value: T | ((prev: T) => T)) => void] {
|
||||
// Implementation
|
||||
}
|
||||
```
|
||||
|
||||
## Utility Types for React
|
||||
|
||||
| Type | Use Case | Example |
|
||||
|------|----------|---------|
|
||||
| `React.ReactNode` | Any renderable content | `children: React.ReactNode` |
|
||||
| `React.ReactElement` | JSX element only (not string/number) | `icon: React.ReactElement` |
|
||||
| `React.ComponentPropsWithRef<'div'>` | All div props including ref | Extending native elements |
|
||||
| `React.MouseEventHandler<HTMLButtonElement>` | Typed event handler | `onClick: React.MouseEventHandler<HTMLButtonElement>` |
|
||||
| `React.ChangeEvent<HTMLInputElement>` | Input change event | `(e: React.ChangeEvent<HTMLInputElement>) => void` |
|
||||
| `React.FormEvent<HTMLFormElement>` | Form submit event | `onSubmit: React.FormEventHandler<HTMLFormElement>` |
|
||||
| `React.CSSProperties` | Inline style object | `style?: React.CSSProperties` |
|
||||
|
||||
## Common Utility Patterns
|
||||
|
||||
### Required Pick
|
||||
Make specific properties required from an otherwise optional interface:
|
||||
```typescript
|
||||
type RequiredName = Required<Pick<UserProps, 'firstName' | 'lastName'>> & Omit<UserProps, 'firstName' | 'lastName'>;
|
||||
```
|
||||
|
||||
### Extract Prop Types from Component
|
||||
```typescript
|
||||
type ButtonProps = React.ComponentProps<typeof Button>;
|
||||
```
|
||||
|
||||
### Async State Pattern
|
||||
```typescript
|
||||
type AsyncState<T> =
|
||||
| { status: 'idle' }
|
||||
| { status: 'loading' }
|
||||
| { status: 'success'; data: T }
|
||||
| { status: 'error'; error: string };
|
||||
```
|
||||
|
||||
## Strict Null Checking Patterns
|
||||
|
||||
### Guard Hooks
|
||||
```typescript
|
||||
function useRequiredContext<T>(context: React.Context<T | null>, name: string): T {
|
||||
const value = useContext(context);
|
||||
if (value === null) throw new Error(`${name} must be used within its Provider`);
|
||||
return value;
|
||||
}
|
||||
```
|
||||
|
||||
### Narrowing with Type Guards
|
||||
```typescript
|
||||
function isUser(value: unknown): value is User {
|
||||
return typeof value === 'object' && value !== null && 'id' in value && 'email' in value;
|
||||
}
|
||||
```
|
||||
|
||||
## Things to Avoid
|
||||
|
||||
| Anti-Pattern | Why | Alternative |
|
||||
|-------------|-----|-------------|
|
||||
| `React.FC` | Implicit children, no generics | Explicit typed function |
|
||||
| `any` for event handlers | Loses type safety | `React.MouseEvent<HTMLButtonElement>` |
|
||||
| `as` for DOM queries | Runtime type mismatch risk | Type guards or `instanceof` |
|
||||
| `!` non-null assertion | Hides potential null bugs | Conditional rendering or optional chaining |
|
||||
| `enum` for prop variants | Not tree-shakeable, numeric by default | String union types |
|
||||
28
plugins/saas-react-platform/skills/visual-header.md
Normal file
28
plugins/saas-react-platform/skills/visual-header.md
Normal file
@@ -0,0 +1,28 @@
|
||||
# Visual Header Skill
|
||||
|
||||
Standard visual header for saas-react-platform commands.
|
||||
|
||||
## Header Template
|
||||
|
||||
```
|
||||
+----------------------------------------------------------------------+
|
||||
| REACT-PLATFORM - [Context] |
|
||||
+----------------------------------------------------------------------+
|
||||
```
|
||||
|
||||
## Context Values by Command
|
||||
|
||||
| Command | Context |
|
||||
|---------|---------|
|
||||
| `/react setup` | Setup Wizard |
|
||||
| `/react component` | Component |
|
||||
| `/react route` | Route |
|
||||
| `/react state` | State Management |
|
||||
| `/react hook` | Custom Hook |
|
||||
| `/react lint` | Lint |
|
||||
| Agent mode (react-architect) | Architecture |
|
||||
| Agent mode (react-auditor) | Audit |
|
||||
|
||||
## Usage
|
||||
|
||||
Display header at the start of every command response before proceeding with the operation.
|
||||
Reference in New Issue
Block a user