diff --git a/docs/superpowers/plans/2026-05-12-grip-replaces-pin.md b/docs/superpowers/plans/2026-05-12-grip-replaces-pin.md new file mode 100644 index 00000000..ca97866c --- /dev/null +++ b/docs/superpowers/plans/2026-05-12-grip-replaces-pin.md @@ -0,0 +1,311 @@ +# Grip-Replaces-Pin Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Eliminate the always-reserved 18px left gutter on pinned thread rows by swapping the drag affordance into the same slot as the pin icon, opacity-toggled on hover. + +**Architecture:** Delete the standalone `.chat-thread-list__grip` button (sibling of the row's main click button). Wrap the existing inline pin SVG in a new `.chat-thread-list__pin-slot` span; render a grip glyph as a positioned sibling inside that slot when the thread is pinned AND `actions.reorderPinned` is defined. CSS opacity-toggle on `:hover` / `:focus-within` performs the swap. The `
  • ` keeps its `draggable` attribute and drag handlers — the entire row remains the drag source. + +**Tech Stack:** Angular 21 standalone components, signals, CSS-in-TS, vitest. + +**Reference spec:** `docs/superpowers/specs/2026-05-12-grip-replaces-pin-design.md` + +--- + +### Task 1: Swap to pin-slot with hover opacity-toggle + +**Files:** +- Modify: `libs/chat/src/lib/primitives/chat-thread-list/chat-thread-list.component.ts` (template, around lines 92–150) +- Modify: `libs/chat/src/lib/styles/chat-thread-list.styles.ts` (replace `.chat-thread-list__grip` rules near lines 126–155) +- Modify: `libs/chat/src/lib/primitives/chat-thread-list/chat-thread-list.component.spec.ts` (add one new structural test) + +**Context:** + +The existing three `chat-thread-list__grip` tests (lines 485–511) use `.querySelector(s)('.chat-thread-list__grip')` to check presence/absence. The new structure keeps the `.chat-thread-list__grip` class name on the in-slot span, so those selectors continue to find the element when it should be there and return null when it shouldn't. **No changes to existing tests are required.** + +The new structural test verifies the wrapping `.chat-thread-list__pin-slot` exists and contains both `.chat-thread-list__item-pin` and `.chat-thread-list__grip` as children for a pinned row with `reorderPinned`. + +The two existing drag-and-drop tests (lines 644, 678) dispatch synthetic events on the `
  • ` wrap, which is untouched — they continue to pass. + +--- + +- [ ] **Step 1: Add the new structural test (failing)** + +Insert immediately after the test on line 511 (after the closing `});` of the "grip handle does NOT render when reorderPinned absent" test) in `libs/chat/src/lib/primitives/chat-thread-list/chat-thread-list.component.spec.ts`: + +```ts +it('pin slot wraps both pin SVG and grip glyph for reorderable pinned row', () => { + const fixture = TestBed.createComponent(ChatThreadListComponent); + fixture.componentRef.setInput('threads', [ + { id: 'p1', title: 'P1', pinned: true }, + ]); + fixture.componentRef.setInput('actions', { reorderPinned: vi.fn().mockResolvedValue(undefined) }); + fixture.detectChanges(); + const slot = fixture.nativeElement.querySelector('.chat-thread-list__pin-slot'); + expect(slot).not.toBeNull(); + expect(slot.querySelector('.chat-thread-list__item-pin')).not.toBeNull(); + expect(slot.querySelector('.chat-thread-list__grip')).not.toBeNull(); +}); + +it('pin slot renders without grip when reorderPinned absent', () => { + const fixture = TestBed.createComponent(ChatThreadListComponent); + fixture.componentRef.setInput('threads', [ + { id: 'p1', title: 'P1', pinned: true }, + ]); + fixture.componentRef.setInput('actions', {}); + fixture.detectChanges(); + const slot = fixture.nativeElement.querySelector('.chat-thread-list__pin-slot'); + expect(slot).not.toBeNull(); + expect(slot.querySelector('.chat-thread-list__item-pin')).not.toBeNull(); + expect(slot.querySelector('.chat-thread-list__grip')).toBeNull(); +}); +``` + +- [ ] **Step 2: Run test to verify it fails** + +``` +cd libs/chat && npx vitest run src/lib/primitives/chat-thread-list/chat-thread-list.component.spec.ts -t "pin slot" +``` + +Expected: FAIL with `expected null not to be null` on the `.chat-thread-list__pin-slot` query. + +- [ ] **Step 3: Update the template** + +In `libs/chat/src/lib/primitives/chat-thread-list/chat-thread-list.component.ts`, locate the block beginning around line 122: + +```html + @if (thread.pinned && actions()?.reorderPinned) { + + } + +} + +``` + +After: + +```html + +``` + +The `
  • ` wrapper, its `[attr.draggable]`, and the five drag handlers (`dragstart`, `dragover`, `dragleave`, `drop`, `dragend`) are unchanged. The grip is now a `` (not a ` - }