238 lines
11 KiB
Markdown
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>
|