docs(20): create phase plan for FAB and full-screen catalog search
This commit is contained in:
@@ -200,7 +200,10 @@ Plans:
|
||||
3. Full-screen catalog search overlay opens from either add option
|
||||
4. Search results display catalog items with name, weight, price, owner count
|
||||
5. Tag chips filter search results
|
||||
**Plans**: TBD
|
||||
**Plans:** 2 plans
|
||||
Plans:
|
||||
- [ ] 20-01-PLAN.md — Tags endpoint, global-items route registration, UIStore extension, useTags hook
|
||||
- [ ] 20-02-PLAN.md — FabMenu component, CatalogSearchOverlay component, root layout wiring
|
||||
**UI hint**: yes
|
||||
|
||||
### Phase 21: Add-from-Catalog & Thread Integration
|
||||
@@ -248,6 +251,6 @@ Plans:
|
||||
| 17. Object Storage | v2.0 | 0/? | Not started | - |
|
||||
| 18. Global Items & Public Profiles | v2.0 | 4/5 | Complete | 2026-04-05 |
|
||||
| 19. Reference Item Model & Tags Schema | v2.0 | 3/3 | Complete | 2026-04-05 |
|
||||
| 20. FAB & Full-Screen Catalog Search | v2.0 | 0/? | Not started | - |
|
||||
| 20. FAB & Full-Screen Catalog Search | v2.0 | 0/2 | Not started | - |
|
||||
| 21. Add-from-Catalog & Thread Integration | v2.0 | 0/? | Not started | - |
|
||||
| 22. Manual Entry Fallback | v2.0 | 0/? | Not started | - |
|
||||
|
||||
279
.planning/phases/20-fab-full-screen-catalog-search/20-01-PLAN.md
Normal file
279
.planning/phases/20-fab-full-screen-catalog-search/20-01-PLAN.md
Normal file
@@ -0,0 +1,279 @@
|
||||
---
|
||||
phase: 20-fab-full-screen-catalog-search
|
||||
plan: 01
|
||||
type: execute
|
||||
wave: 1
|
||||
depends_on: []
|
||||
files_modified:
|
||||
- src/server/services/tag.service.ts
|
||||
- src/server/routes/tags.ts
|
||||
- src/server/index.ts
|
||||
- src/client/stores/uiStore.ts
|
||||
- src/client/hooks/useTags.ts
|
||||
- src/client/hooks/useGlobalItems.ts
|
||||
- tests/services/tag.service.test.ts
|
||||
- tests/routes/tags.test.ts
|
||||
autonomous: true
|
||||
requirements:
|
||||
- CATFLOW-01
|
||||
- CATFLOW-02
|
||||
|
||||
must_haves:
|
||||
truths:
|
||||
- "GET /api/tags returns all tags from the database"
|
||||
- "GET /api/global-items endpoint is reachable (route registered)"
|
||||
- "UIStore exposes fabMenu and catalogSearch state slices"
|
||||
- "useGlobalItems hook supports optional tags parameter"
|
||||
- "useTags hook fetches and caches tag data"
|
||||
artifacts:
|
||||
- path: "src/server/services/tag.service.ts"
|
||||
provides: "getAllTags function"
|
||||
exports: ["getAllTags"]
|
||||
- path: "src/server/routes/tags.ts"
|
||||
provides: "GET /api/tags endpoint"
|
||||
exports: ["tagRoutes"]
|
||||
- path: "src/client/stores/uiStore.ts"
|
||||
provides: "FAB menu + catalog search state"
|
||||
contains: "fabMenuOpen"
|
||||
- path: "src/client/hooks/useTags.ts"
|
||||
provides: "Tag fetching hook"
|
||||
exports: ["useTags"]
|
||||
- path: "src/client/hooks/useGlobalItems.ts"
|
||||
provides: "Updated hook with tag support"
|
||||
contains: "tags"
|
||||
key_links:
|
||||
- from: "src/server/routes/tags.ts"
|
||||
to: "src/server/services/tag.service.ts"
|
||||
via: "getAllTags import"
|
||||
pattern: "getAllTags"
|
||||
- from: "src/server/index.ts"
|
||||
to: "src/server/routes/tags.ts"
|
||||
via: "app.route registration"
|
||||
pattern: "app\\.route.*tags"
|
||||
- from: "src/server/index.ts"
|
||||
to: "src/server/routes/global-items.ts"
|
||||
via: "app.route registration"
|
||||
pattern: "app\\.route.*global-items"
|
||||
---
|
||||
|
||||
<objective>
|
||||
Create the server-side tags endpoint, register the global-items route, extend UIStore with FAB/catalog-search state, and update client hooks for tag-aware searching.
|
||||
|
||||
Purpose: Provides the data layer (tags API), state management (UIStore), and hooks that Plan 02's UI components will consume.
|
||||
Output: Working GET /api/tags endpoint, registered GET /api/global-items, extended UIStore, useTags hook, updated useGlobalItems hook with tag support.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
|
||||
@$HOME/.claude/get-shit-done/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.planning/PROJECT.md
|
||||
@.planning/ROADMAP.md
|
||||
@.planning/STATE.md
|
||||
@.planning/phases/20-fab-full-screen-catalog-search/20-CONTEXT.md
|
||||
@.planning/phases/20-fab-full-screen-catalog-search/20-RESEARCH.md
|
||||
|
||||
@src/server/index.ts
|
||||
@src/server/routes/global-items.ts
|
||||
@src/server/services/global-item.service.ts
|
||||
@src/db/schema.ts
|
||||
@src/client/stores/uiStore.ts
|
||||
@src/client/hooks/useGlobalItems.ts
|
||||
@src/client/lib/api.ts
|
||||
@tests/routes/global-items.test.ts
|
||||
@tests/helpers/db.ts
|
||||
|
||||
<interfaces>
|
||||
<!-- Key types and contracts the executor needs -->
|
||||
|
||||
From src/db/schema.ts:
|
||||
```typescript
|
||||
export const tags = pgTable("tags", {
|
||||
id: serial("id").primaryKey(),
|
||||
name: text("name").notNull().unique(),
|
||||
createdAt: timestamp("created_at").defaultNow().notNull(),
|
||||
});
|
||||
```
|
||||
|
||||
From src/client/lib/api.ts:
|
||||
```typescript
|
||||
export async function apiGet<T>(path: string): Promise<T>;
|
||||
```
|
||||
|
||||
From src/client/stores/uiStore.ts (existing pattern):
|
||||
```typescript
|
||||
export const useUIStore = create<UIState>((set) => ({ ... }));
|
||||
```
|
||||
</interfaces>
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 1: Tag service, route, route registrations, and tests</name>
|
||||
<files>
|
||||
src/server/services/tag.service.ts,
|
||||
src/server/routes/tags.ts,
|
||||
src/server/index.ts,
|
||||
tests/services/tag.service.test.ts,
|
||||
tests/routes/tags.test.ts
|
||||
</files>
|
||||
<read_first>
|
||||
src/db/schema.ts,
|
||||
src/server/index.ts,
|
||||
src/server/routes/global-items.ts,
|
||||
src/server/services/global-item.service.ts,
|
||||
tests/routes/global-items.test.ts,
|
||||
tests/helpers/db.ts
|
||||
</read_first>
|
||||
<behavior>
|
||||
- Test: getAllTags returns all tags from the database as { id, name }[] (no createdAt)
|
||||
- Test: getAllTags returns empty array when no tags exist
|
||||
- Test: GET /api/tags returns 200 with array of tag objects
|
||||
- Test: GET /api/global-items still works after route registration (existing tests pass)
|
||||
</behavior>
|
||||
<action>
|
||||
1. Create `src/server/services/tag.service.ts`:
|
||||
- Export `getAllTags(db)` function that selects `{ id: tags.id, name: tags.name }` from the `tags` table, ordered alphabetically by name (per D-17, alphabetical is a reasonable default).
|
||||
- Import `tags` from `../../db/schema.ts`.
|
||||
- Use the same `type Db = typeof prodDb` pattern as other services.
|
||||
|
||||
2. Create `src/server/routes/tags.ts`:
|
||||
- Single GET "/" handler that calls `getAllTags(db)` and returns `c.json(allTags)`.
|
||||
- Export as `tagRoutes`.
|
||||
- Follow the exact pattern from `src/server/routes/global-items.ts` (Hono + Env type).
|
||||
|
||||
3. Update `src/server/index.ts`:
|
||||
- Add import: `import { globalItemRoutes } from "./routes/global-items.ts";`
|
||||
- Add import: `import { tagRoutes } from "./routes/tags.ts";`
|
||||
- Register both routes AFTER existing route registrations (before MCP block):
|
||||
`app.route("/api/global-items", globalItemRoutes);`
|
||||
`app.route("/api/tags", tagRoutes);`
|
||||
- CRITICAL: The global-items route file EXISTS but is NOT registered (research pitfall #1). Must add it here.
|
||||
- Add public route skip in auth middleware for tags: `if (c.req.path.startsWith("/api/tags") && c.req.method === "GET") return next();`
|
||||
- Add public route skip for global-items: `if (c.req.path.startsWith("/api/global-items") && c.req.method === "GET") return next();`
|
||||
|
||||
4. Create `tests/services/tag.service.test.ts`:
|
||||
- Use `createTestDb()` from `tests/helpers/db.ts`.
|
||||
- Test: returns empty array when no tags.
|
||||
- Test: returns all tags as `{ id, name }` after inserting test data.
|
||||
- Seed tags using `db.insert(tags).values(...)`.
|
||||
|
||||
5. Create `tests/routes/tags.test.ts`:
|
||||
- Follow pattern from `tests/routes/global-items.test.ts`.
|
||||
- Use the app with test db injection.
|
||||
- Test: GET /api/tags returns 200 with JSON array.
|
||||
- Test: response contains expected tags after seeding.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>bun test tests/services/tag.service.test.ts tests/routes/tags.test.ts tests/routes/global-items.test.ts</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- File `src/server/services/tag.service.ts` exists and exports `getAllTags`
|
||||
- File `src/server/routes/tags.ts` exists and exports `tagRoutes`
|
||||
- `src/server/index.ts` contains `app.route("/api/tags", tagRoutes)`
|
||||
- `src/server/index.ts` contains `app.route("/api/global-items", globalItemRoutes)`
|
||||
- `src/server/index.ts` contains auth skip for `/api/tags` and `/api/global-items` GET requests
|
||||
- All tag tests pass: `bun test tests/services/tag.service.test.ts tests/routes/tags.test.ts`
|
||||
- Existing global-items tests still pass: `bun test tests/routes/global-items.test.ts`
|
||||
</acceptance_criteria>
|
||||
<done>GET /api/tags returns all tags. GET /api/global-items is registered and reachable. All tests green.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: UIStore extension + useTags hook + useGlobalItems tag support</name>
|
||||
<files>
|
||||
src/client/stores/uiStore.ts,
|
||||
src/client/hooks/useTags.ts,
|
||||
src/client/hooks/useGlobalItems.ts
|
||||
</files>
|
||||
<read_first>
|
||||
src/client/stores/uiStore.ts,
|
||||
src/client/hooks/useGlobalItems.ts,
|
||||
src/client/lib/api.ts,
|
||||
.planning/phases/20-fab-full-screen-catalog-search/20-CONTEXT.md
|
||||
</read_first>
|
||||
<action>
|
||||
1. Extend `src/client/stores/uiStore.ts` per D-21, D-22, D-23:
|
||||
- Add to UIState interface:
|
||||
```
|
||||
fabMenuOpen: boolean;
|
||||
openFabMenu: () => void;
|
||||
closeFabMenu: () => void;
|
||||
catalogSearchOpen: boolean;
|
||||
catalogSearchMode: "collection" | "thread" | null;
|
||||
openCatalogSearch: (mode: "collection" | "thread") => void;
|
||||
closeCatalogSearch: () => void;
|
||||
```
|
||||
- Add to store implementation:
|
||||
```
|
||||
fabMenuOpen: false,
|
||||
openFabMenu: () => set({ fabMenuOpen: true }),
|
||||
closeFabMenu: () => set({ fabMenuOpen: false }),
|
||||
catalogSearchOpen: false,
|
||||
catalogSearchMode: null,
|
||||
openCatalogSearch: (mode) => set({ catalogSearchOpen: true, catalogSearchMode: mode, fabMenuOpen: false }),
|
||||
closeCatalogSearch: () => set({ catalogSearchOpen: false, catalogSearchMode: null }),
|
||||
```
|
||||
- Note: `openCatalogSearch` also closes the FAB menu (natural flow: tap FAB -> tap option -> menu closes, overlay opens).
|
||||
|
||||
2. Create `src/client/hooks/useTags.ts`:
|
||||
- Export `useTags()` hook using `useQuery` from `@tanstack/react-query`.
|
||||
- Query key: `["tags"]`.
|
||||
- Query fn: `apiGet<Tag[]>("/api/tags")` where `Tag = { id: number; name: string }`.
|
||||
- Set `staleTime: 5 * 60 * 1000` (tags change rarely, cache 5 min).
|
||||
- Export the `Tag` interface as well.
|
||||
|
||||
3. Update `src/client/hooks/useGlobalItems.ts`:
|
||||
- Modify `useGlobalItems` signature to accept optional `tags?: string[]` parameter.
|
||||
- Build query string using `URLSearchParams`:
|
||||
```
|
||||
const params = new URLSearchParams();
|
||||
if (query) params.set("q", query);
|
||||
if (tags && tags.length > 0) params.set("tags", tags.join(","));
|
||||
const qs = params.toString();
|
||||
```
|
||||
- Update query key to include tags: `["global-items", query ?? "", tags ?? []]`.
|
||||
- Update query fn URL: `` `/api/global-items${qs ? `?${qs}` : ""}` ``.
|
||||
- Keep all other exports (`useGlobalItem`, `useLinkItem`, `useUnlinkItem`) unchanged.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>bun run lint && bun run build</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- `src/client/stores/uiStore.ts` contains `fabMenuOpen` state field
|
||||
- `src/client/stores/uiStore.ts` contains `catalogSearchOpen` state field
|
||||
- `src/client/stores/uiStore.ts` contains `catalogSearchMode` state field
|
||||
- `src/client/stores/uiStore.ts` contains `openCatalogSearch` action
|
||||
- `src/client/stores/uiStore.ts` contains `closeCatalogSearch` action
|
||||
- `src/client/hooks/useTags.ts` exists and exports `useTags`
|
||||
- `src/client/hooks/useGlobalItems.ts` accepts `tags?: string[]` parameter
|
||||
- `bun run lint` passes with no errors
|
||||
- `bun run build` succeeds
|
||||
</acceptance_criteria>
|
||||
<done>UIStore has FAB menu + catalog search state. useTags hook fetches tags. useGlobalItems supports tag filtering. Lint and build pass.</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<verification>
|
||||
- `bun test tests/services/tag.service.test.ts tests/routes/tags.test.ts tests/routes/global-items.test.ts` -- all green
|
||||
- `bun run lint` -- no errors
|
||||
- `bun run build` -- succeeds
|
||||
- `bun test` -- full suite green (no regressions)
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
1. GET /api/tags returns all tags as JSON array
|
||||
2. GET /api/global-items is registered and reachable (was missing from index.ts)
|
||||
3. UIStore has fabMenuOpen, catalogSearchOpen, catalogSearchMode state
|
||||
4. useTags hook works with staleTime caching
|
||||
5. useGlobalItems accepts optional tags parameter for filtering
|
||||
6. All existing tests pass (no regressions)
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/20-fab-full-screen-catalog-search/20-01-SUMMARY.md`
|
||||
</output>
|
||||
334
.planning/phases/20-fab-full-screen-catalog-search/20-02-PLAN.md
Normal file
334
.planning/phases/20-fab-full-screen-catalog-search/20-02-PLAN.md
Normal file
@@ -0,0 +1,334 @@
|
||||
---
|
||||
phase: 20-fab-full-screen-catalog-search
|
||||
plan: 02
|
||||
type: execute
|
||||
wave: 2
|
||||
depends_on: ["20-01"]
|
||||
files_modified:
|
||||
- src/client/components/FabMenu.tsx
|
||||
- src/client/components/CatalogSearchOverlay.tsx
|
||||
- src/client/routes/__root.tsx
|
||||
autonomous: false
|
||||
requirements:
|
||||
- CATFLOW-01
|
||||
- CATFLOW-02
|
||||
|
||||
must_haves:
|
||||
truths:
|
||||
- "FAB is visible on all authenticated pages (collection, threads, setups, dashboard, settings, global-items)"
|
||||
- "FAB is NOT visible on login page or public profile/setup pages"
|
||||
- "Tapping FAB opens a mini menu with 'Add to Collection' and 'Start New Thread' options"
|
||||
- "On setups page, FAB menu also shows 'New Setup' option"
|
||||
- "Tapping 'Add to Collection' opens full-screen catalog search overlay in 'collection' mode"
|
||||
- "Tapping 'Start New Thread' opens full-screen catalog search overlay in 'thread' mode"
|
||||
- "Catalog search overlay has search input with debounce, tag chips, and result cards"
|
||||
- "Tag chips toggle on/off and filter search results via AND logic"
|
||||
- "Result cards show brand, model, weight, price, category, and an Add button (stub)"
|
||||
- "Back arrow closes the catalog search overlay"
|
||||
artifacts:
|
||||
- path: "src/client/components/FabMenu.tsx"
|
||||
provides: "FAB with mini menu"
|
||||
min_lines: 60
|
||||
- path: "src/client/components/CatalogSearchOverlay.tsx"
|
||||
provides: "Full-screen catalog search"
|
||||
min_lines: 100
|
||||
key_links:
|
||||
- from: "src/client/components/FabMenu.tsx"
|
||||
to: "src/client/stores/uiStore.ts"
|
||||
via: "useUIStore (fabMenuOpen, openCatalogSearch)"
|
||||
pattern: "useUIStore"
|
||||
- from: "src/client/components/CatalogSearchOverlay.tsx"
|
||||
to: "src/client/hooks/useGlobalItems.ts"
|
||||
via: "useGlobalItems(query, tags)"
|
||||
pattern: "useGlobalItems"
|
||||
- from: "src/client/components/CatalogSearchOverlay.tsx"
|
||||
to: "src/client/hooks/useTags.ts"
|
||||
via: "useTags()"
|
||||
pattern: "useTags"
|
||||
- from: "src/client/routes/__root.tsx"
|
||||
to: "src/client/components/FabMenu.tsx"
|
||||
via: "FabMenu component render"
|
||||
pattern: "FabMenu"
|
||||
- from: "src/client/routes/__root.tsx"
|
||||
to: "src/client/components/CatalogSearchOverlay.tsx"
|
||||
via: "CatalogSearchOverlay component render"
|
||||
pattern: "CatalogSearchOverlay"
|
||||
---
|
||||
|
||||
<objective>
|
||||
Build the FAB mini menu component and full-screen catalog search overlay, then wire them into the root layout.
|
||||
|
||||
Purpose: Delivers the primary user-facing UI for Phase 20 -- the global FAB with action menu and the catalog discovery experience with search + tag filtering.
|
||||
Output: FabMenu.tsx, CatalogSearchOverlay.tsx, updated __root.tsx with new FAB and overlay rendering.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
|
||||
@$HOME/.claude/get-shit-done/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.planning/PROJECT.md
|
||||
@.planning/ROADMAP.md
|
||||
@.planning/STATE.md
|
||||
@.planning/phases/20-fab-full-screen-catalog-search/20-CONTEXT.md
|
||||
@.planning/phases/20-fab-full-screen-catalog-search/20-RESEARCH.md
|
||||
@.planning/phases/20-fab-full-screen-catalog-search/20-01-SUMMARY.md
|
||||
|
||||
@src/client/routes/__root.tsx
|
||||
@src/client/stores/uiStore.ts
|
||||
@src/client/components/GlobalItemCard.tsx
|
||||
@src/client/components/CreateThreadModal.tsx
|
||||
@src/client/hooks/useTags.ts
|
||||
@src/client/hooks/useGlobalItems.ts
|
||||
@src/client/hooks/useFormatters.ts
|
||||
@src/client/routes/global-items/index.tsx
|
||||
|
||||
<interfaces>
|
||||
<!-- From Plan 01 outputs (UIStore extensions) -->
|
||||
From src/client/stores/uiStore.ts (after Plan 01):
|
||||
```typescript
|
||||
// FAB menu state
|
||||
fabMenuOpen: boolean;
|
||||
openFabMenu: () => void;
|
||||
closeFabMenu: () => void;
|
||||
|
||||
// Catalog search state
|
||||
catalogSearchOpen: boolean;
|
||||
catalogSearchMode: "collection" | "thread" | null;
|
||||
openCatalogSearch: (mode: "collection" | "thread") => void;
|
||||
closeCatalogSearch: () => void;
|
||||
```
|
||||
|
||||
From src/client/hooks/useTags.ts (after Plan 01):
|
||||
```typescript
|
||||
export interface Tag { id: number; name: string; }
|
||||
export function useTags(): UseQueryResult<Tag[]>;
|
||||
```
|
||||
|
||||
From src/client/hooks/useGlobalItems.ts (after Plan 01):
|
||||
```typescript
|
||||
export function useGlobalItems(query?: string, tags?: string[]): UseQueryResult<GlobalItem[]>;
|
||||
```
|
||||
|
||||
From src/client/hooks/useFormatters.ts:
|
||||
```typescript
|
||||
export function useFormatters(): { weight: (grams: number) => string; price: (cents: number) => string; };
|
||||
```
|
||||
</interfaces>
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: FabMenu component and CatalogSearchOverlay component</name>
|
||||
<files>
|
||||
src/client/components/FabMenu.tsx,
|
||||
src/client/components/CatalogSearchOverlay.tsx
|
||||
</files>
|
||||
<read_first>
|
||||
src/client/stores/uiStore.ts,
|
||||
src/client/components/GlobalItemCard.tsx,
|
||||
src/client/components/CreateThreadModal.tsx,
|
||||
src/client/routes/global-items/index.tsx,
|
||||
src/client/hooks/useTags.ts,
|
||||
src/client/hooks/useGlobalItems.ts,
|
||||
src/client/hooks/useFormatters.ts,
|
||||
.planning/phases/20-fab-full-screen-catalog-search/20-CONTEXT.md
|
||||
</read_first>
|
||||
<action>
|
||||
**FabMenu.tsx** (per D-01 through D-06, D-23):
|
||||
|
||||
1. Create `src/client/components/FabMenu.tsx` that accepts props: `{ isSetupsPage: boolean }`.
|
||||
2. Read state from UIStore: `fabMenuOpen`, `openFabMenu`, `closeFabMenu`, `openCatalogSearch`, `catalogSearchOpen`.
|
||||
3. The main FAB button: `fixed bottom-6 right-6 z-20 w-14 h-14 bg-gray-700 hover:bg-gray-800 text-white rounded-full shadow-lg`. Shows "+" icon normally, rotates to "x" when menu is open.
|
||||
4. When `fabMenuOpen` is true, render:
|
||||
- A subtle backdrop (`fixed inset-0 z-10 bg-black/20`) that closes menu on click (per D-02).
|
||||
- Menu items stack vertically above the FAB (per D-02), each with an icon + label:
|
||||
- "Add to Collection" with Package icon -- calls `openCatalogSearch("collection")` (per D-04)
|
||||
- "Start New Thread" with Search icon -- calls `openCatalogSearch("thread")` (per D-04)
|
||||
- If `isSetupsPage`, also show "New Setup" with Plus icon -- triggers existing setup creation flow (per D-05). For now, navigate to `/setups` with a query param or call the existing pattern. Read how setups page handles creation and replicate. If unclear, use `navigate({ to: "/setups", search: { action: "create" } })` as a stub.
|
||||
- Menu items positioned `fixed bottom-24 right-6 z-20` (above FAB), each item spaced ~56px apart vertically.
|
||||
5. Use Framer Motion `AnimatePresence` + `motion.div` for menu entrance/exit:
|
||||
- Items appear with `initial={{ opacity: 0, y: 10, scale: 0.9 }}`, `animate={{ opacity: 1, y: 0, scale: 1 }}`, `exit={{ opacity: 0, y: 10, scale: 0.9 }}`.
|
||||
- Transition: `{ type: "spring", stiffness: 400, damping: 25 }` (per research Pattern 5).
|
||||
- Stagger children slightly (50ms delay between items).
|
||||
6. Each menu item: white background with shadow, `rounded-full px-4 py-3 flex items-center gap-3`, icon (20x20) + text label (`text-sm font-medium text-gray-700`).
|
||||
7. Hide FAB entirely when `catalogSearchOpen` is true (per research Pitfall #2 -- overlay covers everything, FAB shouldn't peek through).
|
||||
8. FAB should NOT appear on login page or public routes (per D-06). This is handled by the parent (__root.tsx), not FabMenu itself.
|
||||
|
||||
**CatalogSearchOverlay.tsx** (per D-07 through D-13, D-14 through D-20):
|
||||
|
||||
1. Create `src/client/components/CatalogSearchOverlay.tsx`.
|
||||
2. Read state from UIStore: `catalogSearchOpen`, `catalogSearchMode`, `closeCatalogSearch`.
|
||||
3. Only render when `catalogSearchOpen` is true. Wrap in `AnimatePresence` for enter/exit.
|
||||
4. Overlay container: `fixed inset-0 z-50 bg-white flex flex-col` (per D-07). Full white background, not a backdrop modal.
|
||||
5. Add `overflow-hidden` to document body when overlay is open (per research Pitfall #4). Use `useEffect` to toggle `document.body.style.overflow`.
|
||||
|
||||
**Header section** (per D-08):
|
||||
- Top bar with: back arrow button (left, calls `closeCatalogSearch`), context text ("Adding to Collection" when mode is "collection", "Starting a Thread" when mode is "thread").
|
||||
- Below: large search input (`text-lg px-4 py-3 border-b border-gray-100 w-full`), autofocused, with placeholder "Search the catalog...".
|
||||
- Debounce search input using the established pattern (per research Pattern 4): `useState` for input, `useEffect` with 300ms `setTimeout` for debouncedQuery.
|
||||
|
||||
**Tag chips section** (per D-09, D-14 through D-17):
|
||||
- Below search bar: horizontal scrollable row (`flex gap-2 overflow-x-auto px-4 py-3 no-scrollbar`).
|
||||
- Fetch tags with `useTags()` hook.
|
||||
- Each tag chip: `rounded-full px-3 py-1.5 text-sm font-medium cursor-pointer transition-colors whitespace-nowrap`.
|
||||
- Active state (per D-16): `bg-blue-100 text-blue-700`.
|
||||
- Inactive state (per D-16): `bg-gray-100 text-gray-500`.
|
||||
- Manage selected tags as local state: `useState<string[]>([])`. Toggle tag name on click. Multiple selections = AND filtering (per D-15).
|
||||
- Pass selected tag names to `useGlobalItems(debouncedQuery, selectedTags)`.
|
||||
|
||||
**Results grid** (per D-10, D-18 through D-20):
|
||||
- Grid layout: `grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 p-4 overflow-y-auto flex-1`.
|
||||
- For each result, render a card adapted from `GlobalItemCard` pattern:
|
||||
- Image area (aspect-[4/3] with bg-gray-50 placeholder if no image).
|
||||
- Brand (small, uppercase, gray).
|
||||
- Model name (semibold, truncated).
|
||||
- Badge row: weight (blue), price (green), category (gray) -- same colors as GlobalItemCard.
|
||||
- "Add" button: `bg-gray-700 text-white rounded-lg px-3 py-1.5 text-xs font-medium mt-2`. Per D-19, this is a STUB in Phase 20 -- clicking shows a brief toast or does nothing. Add `onClick` that logs or shows a placeholder message. Do NOT implement actual add-to-collection or add-to-thread flow.
|
||||
- Use `useFormatters()` for weight/price formatting.
|
||||
|
||||
**Loading state** (per D-13):
|
||||
- When query is loading, show 6 skeleton cards matching the grid pattern.
|
||||
- Skeleton card: `animate-pulse` with gray placeholder blocks for image, title, badges.
|
||||
- Copy the skeleton pattern from `src/client/routes/global-items/index.tsx` if available, or create matching pattern.
|
||||
|
||||
**Empty state** (per D-12):
|
||||
- When query returns empty results and not loading: centered message "No items found" with subtle text.
|
||||
- Include a non-functional "Add Manually" text link (`text-blue-600 underline`) that does nothing in Phase 20 (Phase 22 wires it). If Claude judges it premature, omit the link entirely -- this is Claude's discretion per CONTEXT.md.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>bun run lint && bun run build</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- File `src/client/components/FabMenu.tsx` exists with at least 60 lines
|
||||
- File `src/client/components/CatalogSearchOverlay.tsx` exists with at least 100 lines
|
||||
- FabMenu.tsx imports from `framer-motion` (AnimatePresence)
|
||||
- FabMenu.tsx imports `useUIStore` from stores
|
||||
- FabMenu.tsx renders "Add to Collection" and "Start New Thread" menu items
|
||||
- CatalogSearchOverlay.tsx imports `useTags` and `useGlobalItems`
|
||||
- CatalogSearchOverlay.tsx contains `bg-blue-100 text-blue-700` (active tag chip style per D-16)
|
||||
- CatalogSearchOverlay.tsx contains `bg-gray-100 text-gray-500` (inactive tag chip style per D-16)
|
||||
- CatalogSearchOverlay.tsx contains debounce pattern (setTimeout with 300)
|
||||
- CatalogSearchOverlay.tsx contains skeleton/loading state
|
||||
- CatalogSearchOverlay.tsx contains empty state message
|
||||
- `bun run lint` passes
|
||||
- `bun run build` succeeds
|
||||
</acceptance_criteria>
|
||||
<done>FabMenu renders with animated mini menu. CatalogSearchOverlay renders with search, tag chips, result cards, loading/empty states. Both components compile and lint clean.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Wire FabMenu and CatalogSearchOverlay into root layout</name>
|
||||
<files>
|
||||
src/client/routes/__root.tsx
|
||||
</files>
|
||||
<read_first>
|
||||
src/client/routes/__root.tsx,
|
||||
src/client/components/FabMenu.tsx,
|
||||
src/client/components/CatalogSearchOverlay.tsx,
|
||||
src/client/stores/uiStore.ts
|
||||
</read_first>
|
||||
<action>
|
||||
1. In `src/client/routes/__root.tsx`:
|
||||
- Import `FabMenu` from `../components/FabMenu`.
|
||||
- Import `CatalogSearchOverlay` from `../components/CatalogSearchOverlay`.
|
||||
|
||||
2. Remove the old FAB button block (lines ~257-278, the `button` with "Add new item" title and "+" SVG). Replace with `<FabMenu>` component.
|
||||
|
||||
3. Compute FAB visibility (per D-06):
|
||||
- Remove old `showFab` logic (which was collection gear tab only).
|
||||
- New logic:
|
||||
```
|
||||
const isPublicRoute = location.pathname.startsWith("/users/") || location.pathname === "/login";
|
||||
const showFab = isAuthenticated && !isPublicRoute;
|
||||
```
|
||||
- Determine if on setups page: `const isSetupsPage = !!matchRoute({ to: "/setups", fuzzy: true });`
|
||||
|
||||
4. Render FabMenu conditionally:
|
||||
```
|
||||
{showFab && <FabMenu isSetupsPage={isSetupsPage} />}
|
||||
```
|
||||
|
||||
5. Render CatalogSearchOverlay unconditionally (it manages its own visibility via UIStore):
|
||||
```
|
||||
<CatalogSearchOverlay />
|
||||
```
|
||||
Place it after the FabMenu render, before the OnboardingWizard.
|
||||
|
||||
6. Read `catalogSearchOpen` from UIStore. When catalog search is open, the old `openAddPanel` call from the FAB is no longer relevant -- the FabMenu component handles its own actions. Verify the old `openAddPanel` reference on the FAB button is fully removed.
|
||||
|
||||
7. Verify the `isSetupDetail` matchRoute and existing `collectionSearch` logic can be cleaned up if they were only used for FAB visibility. Keep any logic still needed for TotalsBar or other features.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>bun run lint && bun run build</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- `src/client/routes/__root.tsx` imports `FabMenu` from `../components/FabMenu`
|
||||
- `src/client/routes/__root.tsx` imports `CatalogSearchOverlay` from `../components/CatalogSearchOverlay`
|
||||
- `src/client/routes/__root.tsx` does NOT contain the old single-action FAB button (`title="Add new item"`)
|
||||
- `src/client/routes/__root.tsx` contains `<FabMenu` component usage
|
||||
- `src/client/routes/__root.tsx` contains `<CatalogSearchOverlay` component usage
|
||||
- `src/client/routes/__root.tsx` contains public route check: `location.pathname.startsWith("/users/")`
|
||||
- `bun run lint` passes
|
||||
- `bun run build` succeeds
|
||||
</acceptance_criteria>
|
||||
<done>Root layout renders FabMenu on all authenticated routes and CatalogSearchOverlay globally. Old single-action FAB removed. Build passes.</done>
|
||||
</task>
|
||||
|
||||
<task type="checkpoint:human-verify" gate="blocking">
|
||||
<name>Task 3: Visual verification of FAB and catalog search</name>
|
||||
<files>none</files>
|
||||
<action>
|
||||
Human verifies the FAB mini menu and catalog search overlay work correctly across pages and viewports.
|
||||
</action>
|
||||
<what-built>
|
||||
Global FAB with mini menu (Add to Collection, Start Thread, and New Setup on setups page) plus full-screen catalog search overlay with debounced search, tag chip filtering, and result cards.
|
||||
</what-built>
|
||||
<how-to-verify>
|
||||
1. Start dev server: `bun run dev`
|
||||
2. Navigate to collection page (/collection) -- verify FAB (gray circle with "+") is visible at bottom-right
|
||||
3. Click FAB -- verify mini menu appears with "Add to Collection" and "Start New Thread" items, with subtle backdrop
|
||||
4. Click backdrop -- verify menu closes
|
||||
5. Click FAB again, then click "Add to Collection" -- verify full-screen white overlay opens with "Adding to Collection" text, search input (autofocused), and tag chips
|
||||
6. Type a search query -- verify results appear after brief debounce delay (~300ms) as card grid
|
||||
7. Click a tag chip -- verify it toggles to blue active state and filters results
|
||||
8. Click the back arrow -- verify overlay closes
|
||||
9. Navigate to /threads -- verify FAB is still visible
|
||||
10. Navigate to /setups -- verify FAB menu includes third "New Setup" option
|
||||
11. Navigate to /login -- verify FAB is NOT visible
|
||||
12. Check on mobile viewport (use devtools responsive mode) -- verify single column cards, horizontal scrollable tag chips
|
||||
</how-to-verify>
|
||||
<verify>Human confirms all 12 verification steps pass</verify>
|
||||
<done>User approves FAB and catalog search overlay behavior across all pages and viewports.</done>
|
||||
<resume-signal>Type "approved" or describe any issues</resume-signal>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<verification>
|
||||
- `bun run lint` -- no errors
|
||||
- `bun run build` -- succeeds
|
||||
- `bun test` -- full suite green
|
||||
- Visual: FAB visible on authenticated routes, hidden on public routes
|
||||
- Visual: Mini menu opens/closes with animation
|
||||
- Visual: Full-screen overlay with search, tags, results
|
||||
- Visual: Tag filtering works with AND logic
|
||||
- Visual: Responsive grid (1 col mobile, 2-3 col desktop)
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
1. FAB visible on all authenticated pages with mini menu showing correct options (per D-01 through D-06, CATFLOW-01)
|
||||
2. "New Setup" appears only on setups page (per D-03, CATFLOW-01)
|
||||
3. Full-screen catalog search overlay opens from both add options (per D-07, CATFLOW-02)
|
||||
4. Search with debounce queries existing API (per D-08, D-11, CATFLOW-02)
|
||||
5. Tag chips filter results with AND logic (per D-14 through D-17, CATFLOW-02)
|
||||
6. Result cards display brand, model, weight, price, with stub Add button (per D-10, D-18 through D-20)
|
||||
7. Loading skeletons and empty state present (per D-12, D-13)
|
||||
8. Human verification approved
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/20-fab-full-screen-catalog-search/20-02-SUMMARY.md`
|
||||
</output>
|
||||
Reference in New Issue
Block a user