Files
GearBox/.planning/phases/06-category-icons/06-02-PLAN.md

238 lines
11 KiB
Markdown

---
phase: 06-category-icons
plan: 02
type: execute
wave: 2
depends_on: [06-01]
files_modified:
- src/client/components/IconPicker.tsx
- src/client/components/CategoryPicker.tsx
- src/client/components/CategoryHeader.tsx
- src/client/components/OnboardingWizard.tsx
- src/client/components/CreateThreadModal.tsx
autonomous: true
requirements: [CAT-01]
must_haves:
truths:
- "User can open an icon picker popover and browse Lucide icons organized by group tabs"
- "User can search icons by name/keyword and results filter in real time"
- "User can select a Lucide icon when creating a new category inline (CategoryPicker)"
- "User can select a Lucide icon when editing a category (CategoryHeader)"
- "User can select a Lucide icon during onboarding category creation"
- "Category picker combobox shows Lucide icon + name for each category (not emoji)"
artifacts:
- path: "src/client/components/IconPicker.tsx"
provides: "Lucide icon picker popover component"
min_lines: 150
- path: "src/client/components/CategoryPicker.tsx"
provides: "Updated category combobox with icon display"
contains: "LucideIcon"
- path: "src/client/components/CategoryHeader.tsx"
provides: "Updated category header with icon display and IconPicker for editing"
contains: "IconPicker"
key_links:
- from: "src/client/components/IconPicker.tsx"
to: "src/client/lib/iconData.ts"
via: "import"
pattern: "iconGroups.*iconData"
- from: "src/client/components/CategoryPicker.tsx"
to: "src/client/components/IconPicker.tsx"
via: "import"
pattern: "IconPicker"
- from: "src/client/components/CategoryHeader.tsx"
to: "src/client/components/IconPicker.tsx"
via: "import"
pattern: "IconPicker"
---
<objective>
Build the IconPicker component and update all category create/edit components to use Lucide icons instead of emoji.
Purpose: Enable users to browse, search, and select Lucide icons when creating or editing categories. This is the primary user-facing feature of the phase.
Output: New IconPicker component, updated CategoryPicker, CategoryHeader, OnboardingWizard, and CreateThreadModal.
</objective>
<execution_context>
@/home/jean-luc-makiola/.claude/get-shit-done/workflows/execute-plan.md
@/home/jean-luc-makiola/.claude/get-shit-done/templates/summary.md
</execution_context>
<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/06-category-icons/06-CONTEXT.md
@.planning/phases/06-category-icons/06-01-SUMMARY.md
<interfaces>
<!-- From Plan 01 outputs -->
From src/client/lib/iconData.ts (created in Plan 01):
```typescript
export interface IconEntry { name: string; keywords: string[] }
export interface IconGroup { name: string; icon: string; icons: IconEntry[] }
export const iconGroups: IconGroup[];
export const EMOJI_TO_ICON_MAP: Record<string, string>;
interface LucideIconProps { name: string; size?: number; className?: string; }
export function LucideIcon({ name, size, className }: LucideIconProps): JSX.Element;
```
From src/shared/schemas.ts (updated in Plan 01):
```typescript
export const createCategorySchema = z.object({
name: z.string().min(1, "Category name is required"),
icon: z.string().min(1).max(50).default("package"),
});
export const updateCategorySchema = z.object({
id: z.number().int().positive(),
name: z.string().min(1).optional(),
icon: z.string().min(1).max(50).optional(),
});
```
From src/client/components/EmojiPicker.tsx (EXISTING - architecture reference, will be replaced):
```typescript
interface EmojiPickerProps {
value: string;
onChange: (emoji: string) => void;
size?: "sm" | "md";
}
// Uses: createPortal, click-outside, escape key, search, category tabs, positioned popover
```
</interfaces>
</context>
<tasks>
<task type="auto">
<name>Task 1: Create IconPicker component</name>
<files>src/client/components/IconPicker.tsx</files>
<action>
Create `src/client/components/IconPicker.tsx` following the same portal-based popover pattern as EmojiPicker.tsx but rendering Lucide icons.
Props interface:
```typescript
interface IconPickerProps {
value: string; // Current icon name (kebab-case, e.g. "tent")
onChange: (icon: string) => void;
size?: "sm" | "md";
}
```
Architecture (mirror EmojiPicker exactly for these behaviors):
- Portal-based popover via `createPortal(popup, document.body)`
- Trigger button: bordered square box showing the selected LucideIcon, or "+" when empty
- Position calculation: measure trigger rect, place below (or above if not enough space), clamp left to viewport
- Click-outside detection via document mousedown listener
- Escape key closes popover
- Focus search input on open
- `data-icon-picker` attribute on popover div (for click-outside exclusion in CategoryPicker)
- Stop mousedown propagation from popover (so parent click-outside handlers don't fire)
Popover content:
- Search input at top (placeholder: "Search icons...")
- Group tabs below search (only shown when not searching). Each tab shows a small LucideIcon for that group's `icon` field. Active tab highlighted with blue.
- Icon grid: 6 columns. Each cell renders a LucideIcon at 20px, with hover highlight, name as title attribute. On click, call `onChange(icon.name)` and close.
- When searching: filter across all groups by matching query against icon `name` and `keywords`. Show flat grid of results. Show "No icons found" if empty.
- Popover width: ~w-72 (288px). Max grid height: ~max-h-56 with overflow-y-auto.
Trigger button styling:
- `size="md"`: `w-12 h-12` — icon at 24px inside, gray-500 color
- `size="sm"`: `w-10 h-10` — icon at 20px inside, gray-500 color
- Border, rounded-md, hover:border-gray-300, hover:bg-gray-50
Import `iconGroups` and `LucideIcon` from `../lib/iconData`.
Import `icons` from `lucide-react` only if needed for tab icons (or just use LucideIcon component for tabs too).
</action>
<verify>
<automated>bun run build 2>&1 | tail -5</automated>
</verify>
<done>
- IconPicker component renders a trigger button with the selected Lucide icon
- Clicking trigger opens a portal popover with search + group tabs + icon grid
- Search filters icons across all groups by name and keywords
- Selecting an icon calls onChange and closes popover
- Click-outside and Escape close the popover
- Build succeeds
</done>
</task>
<task type="auto">
<name>Task 2: Update CategoryPicker, CategoryHeader, OnboardingWizard, and CreateThreadModal</name>
<files>
src/client/components/CategoryPicker.tsx,
src/client/components/CategoryHeader.tsx,
src/client/components/OnboardingWizard.tsx,
src/client/components/CreateThreadModal.tsx
</files>
<action>
**CategoryPicker.tsx:**
1. Replace `import { EmojiPicker }` with `import { IconPicker }` from `./IconPicker` and `import { LucideIcon }` from `../lib/iconData`.
2. Change state: `newCategoryEmoji` -> `newCategoryIcon`, default from `"📦"` to `"package"`.
3. In `handleConfirmCreate`: Change `{ name, emoji: newCategoryIcon }` to `{ name, icon: newCategoryIcon }`.
4. In click-outside handler: Change `data-emoji-picker` check to `data-icon-picker`.
5. Reset: Change all `setNewCategoryEmoji("📦")` to `setNewCategoryIcon("package")`.
6. In the combobox input display (when closed): Replace `${selectedCategory.emoji} ` text prefix with nothing — instead, add a LucideIcon before the input or use a different display approach. Best approach: when not open and a category is selected, show a small LucideIcon (size 16, className="text-gray-500 inline") before the category name in the input value.
Actually, for simplicity with the input element, render the icon as a visual prefix:
- Wrap input in a div with `relative` positioning
- Add a `LucideIcon` absolutely positioned on the left (pl-8 on input for padding)
- Input value when closed: just `selectedCategory.name` (no emoji prefix)
- Only show the icon prefix when a category is selected and dropdown is closed
7. In the dropdown list items: Replace `{cat.emoji} {cat.name}` with `<LucideIcon name={cat.icon} size={16} className="inline-block mr-1.5 text-gray-500" /> {cat.name}`.
8. In the inline create flow: Replace `<EmojiPicker value={newCategoryEmoji} onChange={setNewCategoryEmoji} size="sm" />` with `<IconPicker value={newCategoryIcon} onChange={setNewCategoryIcon} size="sm" />`.
**CategoryHeader.tsx:**
1. Replace `import { EmojiPicker }` with `import { IconPicker }` from `./IconPicker` and `import { LucideIcon }` from `../lib/iconData`.
2. Props: rename `emoji` to `icon` (type stays string).
3. State: `editEmoji` -> `editIcon`.
4. In `handleSave`: Change `emoji: editEmoji` to `icon: editIcon`.
5. Edit mode: Replace `<EmojiPicker value={editEmoji} onChange={setEditEmoji} size="sm" />` with `<IconPicker value={editIcon} onChange={setEditIcon} size="sm" />`.
6. Display mode: Replace `<span className="text-xl">{emoji}</span>` with `<LucideIcon name={icon} size={22} className="text-gray-500" />`.
7. Edit button onClick: Change `setEditEmoji(emoji)` to `setEditIcon(icon)`.
**OnboardingWizard.tsx:**
1. Replace `import { EmojiPicker }` with `import { IconPicker }` from `./IconPicker`.
2. State: `categoryEmoji` -> `categoryIcon`, default from `""` to `""` (empty is fine, picker shows "+").
3. In `handleCreateCategory`: Change `emoji: categoryEmoji.trim() || undefined` to `icon: categoryIcon.trim() || undefined`.
4. In step 2 JSX: Change label from "Emoji (optional)" to "Icon (optional)". Replace `<EmojiPicker value={categoryEmoji} onChange={setCategoryEmoji} size="md" />` with `<IconPicker value={categoryIcon} onChange={setCategoryIcon} size="md" />`.
**CreateThreadModal.tsx:**
1. Import `LucideIcon` from `../lib/iconData`.
2. In the category list: Replace `{cat.emoji} {cat.name}` with `<LucideIcon name={cat.icon} size={16} className="inline-block mr-1.5 text-gray-500" /> {cat.name}`.
</action>
<verify>
<automated>bun run build 2>&1 | tail -5</automated>
</verify>
<done>
- CategoryPicker shows Lucide icons inline for each category and uses IconPicker for inline create
- CategoryHeader displays Lucide icon in view mode and offers IconPicker in edit mode
- OnboardingWizard uses IconPicker for category creation step
- CreateThreadModal shows Lucide icons next to category names
- No remaining imports of EmojiPicker in these files
- Build succeeds
</done>
</task>
</tasks>
<verification>
- `bun run build` succeeds
- No TypeScript errors related to emoji/icon types
- No remaining imports of EmojiPicker in modified files
</verification>
<success_criteria>
- IconPicker component exists with search, group tabs, and icon grid
- All category create/edit flows use IconPicker instead of EmojiPicker
- Category display in pickers and headers shows Lucide icons
- Build succeeds without errors
</success_criteria>
<output>
After completion, create `.planning/phases/06-category-icons/06-02-SUMMARY.md`
</output>