From dd882962a102189ff91151f9a6bebbff2c2268aa Mon Sep 17 00:00:00 2001 From: Brian Love Date: Tue, 12 May 2026 21:21:45 -0700 Subject: [PATCH 1/3] docs: spec for grip-replaces-pin-on-hover Eliminate the always-reserved 18px left gutter that pinned rows take for the grip button. Swap the drag affordance into the same slot as the pin icon via opacity transition. Zero layout shift, zero new state. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../2026-05-12-grip-replaces-pin-design.md | 215 ++++++++++++++++++ 1 file changed, 215 insertions(+) create mode 100644 docs/superpowers/specs/2026-05-12-grip-replaces-pin-design.md diff --git a/docs/superpowers/specs/2026-05-12-grip-replaces-pin-design.md b/docs/superpowers/specs/2026-05-12-grip-replaces-pin-design.md new file mode 100644 index 00000000..4c80a95c --- /dev/null +++ b/docs/superpowers/specs/2026-05-12-grip-replaces-pin-design.md @@ -0,0 +1,215 @@ +# Grip-Replaces-Pin on Hover — Design + +**Status:** Approved +**Date:** 2026-05-12 +**Scope:** `libs/chat` thread-list row layout +**Predecessor:** PR #280 (drag-to-reorder pinned threads) + +## Goal + +Eliminate the 18px left-side gutter that pinned thread rows currently reserve for the always-present (but usually invisible) drag-handle button. Swap the drag affordance into the same slot as the pin icon so there is zero layout shift between unpinned, pinned-resting, and pinned-hover states. + +## Background + +PR #280 introduced a grip-handle button as a sibling of the row's main click button. The grip is `width: 16px; margin-right: 2px; opacity: 0` by default, fading in on `:hover` / `:focus-within`. Because the element takes inline space regardless of opacity, every pinned row sits 18px to the right of every unpinned row, which the user dislikes. + +Pinned rows also display a 13×13px pin SVG inline with the title text inside `.chat-thread-list__item-title`. That existing slot — already reserved by the layout when a row is pinned — is the natural place to host the drag affordance. + +## Approach + +Single slot, opacity-swap. Both icons occupy a fixed-size positioned container; CSS toggles which one is visible based on hover state. + +- **Default (pinned row):** pin SVG at full opacity, grip glyph at zero opacity. +- **Hover/focus-within of pinned row that has `actions.reorderPinned`:** pin SVG fades to zero opacity, grip glyph fades to full opacity. Cursor on the wrap becomes `grab`. +- **Active (mid-drag):** cursor becomes `grabbing`. + +Zero layout shift in any state. Unpinned rows are unaffected and unchanged. + +## Architecture + +One template restructure + one styles diff: + +- **`libs/chat/src/lib/primitives/chat-thread-list/chat-thread-list.component.ts`** — remove the standalone `.chat-thread-list__grip` button (currently a sibling of `.chat-thread-list__item`). Wrap the existing inline pin SVG in a new `.chat-thread-list__pin-slot` span. When the thread is pinned AND `actions.reorderPinned` is defined, render a grip glyph as a sibling inside the same slot. The `
  • ` keeps its `draggable` attribute and all five drag handlers untouched — the entire row remains the drag source. + +- **`libs/chat/src/lib/styles/chat-thread-list.styles.ts`** — delete the existing `.chat-thread-list__grip` rule and its hover-reveal selectors. Add new rules for `.chat-thread-list__pin-slot` (position relative, fixed 13×13 dimensions) and the new in-slot `.chat-thread-list__grip` (position absolute, opacity 0). Add hover/focus-within rules to swap opacities, and a wrap-level `cursor: grab` rule when the row is pinned and drag-reorderable. + +## Data Flow + +None. Pure CSS opacity transitions. No new component state, no new inputs, no new outputs. + +## Template + +Before (the relevant fragment inside the `@for` loop): + +```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 ` + } + - }