Adding a New Feature Module
Standard patterns for creating new feature slices in On Book Pro.
Overview
Feature modules in On Book Pro follow a co-located architecture where domain logic, components, and state live together in src/features/<feature-name>/.
Directory Structure
src/features/<feature-name>/
├── components/ # React components for this feature
│ ├── FeatureTab.tsx # Main tab component
│ └── ...
├── store.ts # Zustand slice (state + actions)
├── types.ts # TypeScript interfaces
├── permissions.ts # Permission constants (optional)
├── utils.ts # Feature-specific utilities (optional)
└── index.ts # Public API barrel exportStep 1: Define Types
typescript
// src/features/my-feature/types.ts
import { SceneId } from '@/types/branded';
export interface MyItem {
id: string;
name: string;
sceneId?: SceneId;
createdAt: string;
}Step 2: Create Store Slice
typescript
// src/features/my-feature/store.ts
import { StateCreator } from 'zustand';
import { AppState } from '@/store/types';
import { MyItem } from './types';
export interface MyFeatureState {
myItems: MyItem[];
}
export interface MyFeatureActions {
addMyItem: (item: Omit<MyItem, 'id'>) => void;
updateMyItem: (id: string, updates: Partial<MyItem>) => void;
deleteMyItem: (id: string) => void;
}
export const createMyFeatureSlice: StateCreator<
AppState,
[['zustand/immer', never]],
[],
MyFeatureState & MyFeatureActions
> = (set) => ({
// Initial state
myItems: [],
// Actions
addMyItem: (item) =>
set((state) => {
state.myItems.push({
...item,
id: crypto.randomUUID(),
});
}),
updateMyItem: (id, updates) =>
set((state) => {
const item = state.myItems.find((i) => i.id === id);
if (item) Object.assign(item, updates);
}),
deleteMyItem: (id) =>
set((state) => {
state.myItems = state.myItems.filter((i) => i.id !== id);
}),
});Step 3: Register in Global Store
typescript
// src/store/useAppStore.ts
import { createMyFeatureSlice } from '../features/my-feature/store';
export const useAppStore = create<AppState>()(
devtools(
temporal(
persist(
immer((...a) => ({
// Existing slices...
...createMyFeatureSlice(...a), // Add your slice
})),
// ...
)
)
)
);Also update src/store/types.ts to include your state interface.
Step 4: Create Main Component
tsx
// src/features/my-feature/components/MyFeatureTab.tsx
import { useAppStore } from '@/store/useAppStore';
import { Button, Card, Input } from '@/components/common';
export const MyFeatureTab = () => {
const items = useAppStore((s) => s.myItems);
const addMyItem = useAppStore((s) => s.addMyItem);
return (
<div className="p-4">
<Button leftIcon="add" onClick={() => addMyItem({ name: 'New', createdAt: new Date().toISOString() })}>
Add Item
</Button>
<div className="grid gap-4 mt-4">
{items.map((item) => (
<Card key={item.id}>
<h3>{item.name}</h3>
</Card>
))}
</div>
</div>
);
};Step 5: Define Permissions (if needed)
typescript
// src/features/my-feature/permissions.ts
import { Permission } from '@/features/auth/types';
export const MY_FEATURE_PERMISSIONS = {
view: Permission.VIEW_ALL,
edit: Permission.EDIT_MY_FEATURE, // Add to Permission enum first
};Step 6: Register in Workspace
Add your feature to the workspace configuration:
typescript
// src/features/layout/workspace-config.ts
export const WORKSPACE_TOOLS: Record<WorkspaceId, Tool[]> = {
production: [
// Existing tools...
{
id: 'my-feature',
label: 'My Feature',
icon: 'category',
component: lazy(() => import('@/features/my-feature/components/MyFeatureTab')),
permission: Permission.VIEW_ALL,
},
],
};Step 7: Export Public API
typescript
// src/features/my-feature/index.ts
export { MyFeatureTab } from './components/MyFeatureTab';
export { createMyFeatureSlice } from './store';
export type { MyItem, MyFeatureState, MyFeatureActions } from './types';Checklist
- [ ] Types defined in
types.ts - [ ] Slice created in
store.ts - [ ] Slice registered in
useAppStore.ts - [ ] Store types updated in
src/store/types.ts - [ ] Main component created
- [ ] Permissions defined (if applicable)
- [ ] Tool registered in workspace config
- [ ] Barrel export in
index.ts
Last updated: January 2026