Skip to content

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 export

Step 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