# App Conventions

Patterns and conventions for building apps in the `officerdev` workspace. These complement the root `CLAUDE.md` and `CONVENTIONS.md`.

## Manager Pattern

Each app has a central hook that owns all state, effects, and handlers. Components receive this single object as a prop and destructure what they need internally.

```tsx
// Hook
export const useFileBrowserApp = (basePath: string) => {
  const [entries, setEntries] = useState<DirEntry[]>([]);
  const [loading, setLoading] = useState(true);
  // ... all state, effects, handlers

  return { entries, loading, refresh, handleOpen, /* ... */ };
};

export type UseFileBrowserAppType = ReturnType<typeof useFileBrowserApp>;
```

```tsx
// Component receives the manager as a single prop
type ToolbarProps = {
  fileBrowserManager: UseFileBrowserAppType;
};

export const Toolbar = ({ fileBrowserManager }: ToolbarProps) => {
  const { searchQuery, setSearchQuery, viewMode, setViewMode } = fileBrowserManager;
  // ...
};
```

```tsx
// Root component creates the manager and passes it down
export const FileBrowserApp = ({ basePath = '/' }: FileBrowserAppProps) => {
  const fileBrowserManager = useFileBrowserApp(basePath);

  return (
    <div className="flex flex-col h-full overflow-hidden">
      <Toolbar fileBrowserManager={fileBrowserManager} />
      <FileViewContainer fileBrowserManager={fileBrowserManager} />
    </div>
  );
};
```

**Exception**: Presentational leaf components that receive computed/derived values (e.g. `FileItem`, `EntryRow`) keep individual props since they don't interact with the manager directly.

## Directory Structure

Apps live in `src/apps/<AppName>/` with this layout:

```
FileBrowser/
├── index.ts                  # Public barrel (exports for external consumers)
├── useFiles.ts               # Shared API hooks
├── useTasks.ts
├── FileBrowserApp/
│   ├── index.ts              # Re-exports FileBrowserApp
│   ├── FileBrowserApp.tsx    # Slim composition root
│   ├── useFileBrowserApp.ts  # Manager hook
│   └── components/
│       ├── Breadcrumb.tsx
│       ├── FileGrid.tsx
│       ├── FileViewContainer.tsx
│       └── Toolbar/
│           ├── index.ts
│           ├── Toolbar.tsx
│           ├── ActionButtons.tsx
│           ├── SelectionActions.tsx
│           └── DefaultActions.tsx
└── FileBrowserWidget/
    ├── index.ts
    ├── FileBrowserWidget.tsx
    ├── useFileBrowserWidget.ts
    └── components/
        ├── Header.tsx
        ├── BrowseTab.tsx
        └── EntryRow.tsx
```

Key rules:
- Feature directories use `Feature/Feature.tsx` + `Feature/index.ts` barrel
- Components that warrant further breakdown get their own directory (e.g. `Toolbar/`)
- Shared components can be imported across sibling apps (e.g. Widget imports `Breadcrumb` from App)
- The root `index.ts` is the public API — only export what external consumers need

## File Ordering

Within a file, order sections as:

1. Imports (types → external → workspace → relative)
2. Constants
3. Main export (component or hook)
4. `ReturnType` export (for hooks)
5. Types (props types for sub-components)
6. Helper components / functions

## Prefer plain values over memoization

Default to writing derivations plainly — most are cheap, and the memo costs more than the work. But
`useMemo`/`useCallback` are ordinary tools, not forbidden ones: reach for them when a value or callback
feeds a dependency array, when a computation is genuinely expensive, or when the prop goes to a memoized
child. See `CONVENTIONS.md` for the full rule and why the old "never, React 19 handles it" version was
wrong — the React Compiler is opt-in and is not installed here.

The examples below are still the right default shape:

```tsx
// Plain function — not wrapped in useCallback
const refresh = async () => {
  setLoading(true);
  const data = await files.listDir(currentPath);
  setEntries(data.entries);
  setLoading(false);
};

// Inline ref callback — not wrapped in useCallback
<input ref={(node) => {
  inputRef.current = node;
  if (node) node.setAttribute('webkitdirectory', '');
}} />

// IIFE for derived values — not wrapped in useMemo
const sorted = (() => {
  const compare = (a: DirEntry, b: DirEntry): number => { /* ... */ };
  return [...entries].sort(compare);
})();

// Simple derivations — just inline
const svg = getIcon(name).svg;
const isSearching = searchQuery.trim().length > 0;
```

## Component Extraction

Break monolithic components into focused sub-components in a `components/` directory. Each handles its own null guards internally.

```tsx
// Component handles its own visibility logic
export const UploadProgress = ({ fileBrowserManager }: UploadProgressProps) => {
  const { uploadProgress } = fileBrowserManager;
  if (uploadProgress === null) return null;
  return <div>/* progress bar */</div>;
};

// Parent stays clean — no conditional rendering
<UploadProgress fileBrowserManager={fileBrowserManager} />
```

## Keep It Simple

- Use `prompt()` for simple user input instead of inline forms with state management
- Group hidden `<input type="file">` elements inside their trigger buttons
- Don't over-decompose — if a component is under ~30 lines of JSX, it probably doesn't need extraction
- Prefer early returns over nested ternaries for loading/empty states
