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) {
+ ⋮⋮
+ }
+
+ @if (thread.pinned) {
+
+
+
+ }
+ {{ threadLabel(thread) }}
+
+```
+
+Replace with:
+
+```html
+
+ @if (thread.pinned) {
+
+
+
+
+ @if (actions()?.reorderPinned) {
+ ⋮⋮
+ }
+
+ }
+ {{ threadLabel(thread) }}
+
+```
+
+Note: the `aria-hidden` moves from the inner `` to the outer slot `` (covers both children); the SVG keeps its existing attributes minus its own aria-hidden (now redundant).
+
+- [ ] **Step 4: Update the styles**
+
+In `libs/chat/src/lib/styles/chat-thread-list.styles.ts`, locate this block (around lines 126–155):
+
+```css
+ .chat-thread-list__grip {
+ flex-shrink: 0;
+ width: 16px;
+ height: 28px;
+ margin-right: 2px;
+ padding: 0;
+ border: 0;
+ background: transparent;
+ color: var(--ngaf-chat-text-muted);
+ cursor: grab;
+ opacity: 0;
+ transition: opacity 100ms ease;
+ font-size: 11px;
+ line-height: 1;
+ letter-spacing: -1px;
+ user-select: none;
+ }
+ .chat-thread-list__item-wrap:hover .chat-thread-list__grip,
+ .chat-thread-list__item-wrap:focus-within .chat-thread-list__grip {
+ opacity: 1;
+ }
+ .chat-thread-list__grip:active { cursor: grabbing; }
+```
+
+Replace with:
+
+```css
+ .chat-thread-list__pin-slot {
+ position: relative;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 13px;
+ height: 13px;
+ margin-right: 4px;
+ flex-shrink: 0;
+ vertical-align: -1px;
+ }
+ .chat-thread-list__pin-slot .chat-thread-list__item-pin {
+ position: absolute;
+ inset: 0;
+ width: 13px;
+ height: 13px;
+ opacity: 1;
+ transition: opacity 100ms ease;
+ }
+ .chat-thread-list__pin-slot .chat-thread-list__grip {
+ position: absolute;
+ inset: 0;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ color: var(--ngaf-chat-text-muted);
+ font-size: 11px;
+ line-height: 1;
+ letter-spacing: -1px;
+ opacity: 0;
+ transition: opacity 100ms ease;
+ user-select: none;
+ pointer-events: none;
+ }
+ .chat-thread-list__item-wrap:hover .chat-thread-list__pin-slot .chat-thread-list__item-pin,
+ .chat-thread-list__item-wrap:focus-within .chat-thread-list__pin-slot .chat-thread-list__item-pin {
+ opacity: 0;
+ }
+ .chat-thread-list__item-wrap:hover .chat-thread-list__pin-slot .chat-thread-list__grip,
+ .chat-thread-list__item-wrap:focus-within .chat-thread-list__pin-slot .chat-thread-list__grip {
+ opacity: 1;
+ }
+ .chat-thread-list__item-wrap[draggable="true"] { cursor: grab; }
+ .chat-thread-list__item-wrap[draggable="true"]:active { cursor: grabbing; }
+```
+
+- [ ] **Step 5: Run new tests to verify they pass**
+
+```
+cd libs/chat && npx vitest run src/lib/primitives/chat-thread-list/chat-thread-list.component.spec.ts -t "pin slot"
+```
+
+Expected: PASS — both new tests green.
+
+- [ ] **Step 6: Run the full chat-thread-list spec to confirm no regression**
+
+```
+cd libs/chat && npx vitest run src/lib/primitives/chat-thread-list/chat-thread-list.component.spec.ts
+```
+
+Expected: PASS — all tests including the original three grip tests and the two drag-and-drop tests.
+
+- [ ] **Step 7: Run the full chat suite**
+
+```
+cd libs/chat && npx vitest run
+```
+
+Expected: PASS — every chat spec.
+
+- [ ] **Step 8: Manual smoke verification**
+
+(If the implementer can run the dev server. Otherwise document for the reviewer in PR description.)
+
+1. Start the chat example dev server.
+2. Pin one thread.
+3. **Verify:** Pinned row's left edge aligns with unpinned rows' left edge — no gutter shift.
+4. **Hover** the pinned row.
+5. **Verify:** Pin icon fades out, grip glyph `⋮⋮` fades in over the same 13×13 slot. Cursor changes to `grab`.
+6. Pin a second thread; drag-reorder.
+7. **Verify:** Drag indicator, drop, and reorder persistence all behave as before PR #280.
+
+- [ ] **Step 9: Commit**
+
+```bash
+git add libs/chat/src/lib/primitives/chat-thread-list/chat-thread-list.component.ts \
+ libs/chat/src/lib/styles/chat-thread-list.styles.ts \
+ libs/chat/src/lib/primitives/chat-thread-list/chat-thread-list.component.spec.ts
+git commit -m "$(cat <<'EOF'
+fix(chat): drag handle replaces pin icon on hover (no left-gutter shift)
+
+PR #280 introduced a standalone grip button as a sibling of the row's
+main click button. The grip occupies 18px (16px width + 2px margin)
+of inline space whether visible or not, so pinned rows sat 18px right
+of unpinned rows. The user disliked the constant layout shift.
+
+Drop the standalone grip. Wrap the existing pin SVG in a fixed-size
+.chat-thread-list__pin-slot span; render the grip glyph as an
+absolutely-positioned sibling inside that slot. CSS opacity-toggle on
+:hover / :focus-within swaps which icon is visible. Zero layout
+shift, same affordance.
+
+The remains the drag source — all drag handlers untouched. Move
+up / Move down menu items (keyboard path) unchanged.
+
+EOF
+)"
+```
+
+- [ ] **Step 10: Push and open PR**
+
+```bash
+git push -u origin claude/grip-replaces-pin
+gh pr create --title "fix(chat): drag handle replaces pin icon on hover (no left-gutter shift)" --body "$(cat <<'EOF'
+## Summary
+
+- Drops the standalone `.chat-thread-list__grip` button that PR #280 introduced as a sibling of the row's main click button
+- Wraps the existing pin SVG in a new `.chat-thread-list__pin-slot` (13×13px); renders the grip glyph as an absolutely-positioned sibling inside the same slot
+- CSS opacity-toggles on `:hover` / `:focus-within` swap which icon is visible
+- The ` ` wrap remains the drag source — all drag handlers untouched
+
+**Result:** zero left-gutter layout shift between unpinned, pinned, and pinned-hover states.
+
+## Spec & Plan
+
+- `docs/superpowers/specs/2026-05-12-grip-replaces-pin-design.md`
+- `docs/superpowers/plans/2026-05-12-grip-replaces-pin.md`
+
+## Test plan
+
+- [x] Two new vitest tests assert the pin-slot structure
+- [x] All five PR #280 tests (grip presence, drag-and-drop, Move up/down) continue to pass
+- [ ] Manual (reviewer):
+ - [ ] Pinned row left edge aligns with unpinned rows (no gutter)
+ - [ ] Hover pinned row → pin icon fades to grip glyph in same slot
+ - [ ] Drag-reorder still works
+ - [ ] Move up / Move down menu items still work (keyboard path)
+EOF
+)"
+```
+
+---
+
+## Self-review notes
+
+- **Spec coverage:** template change (steps 3), styles change (step 4), tests (steps 1–2, 5–7), accessibility (`aria-hidden` move documented in step 3 + pointer-events on grip in step 4), manual verification (step 8). Complete.
+- **No placeholders:** every code block is exact final content.
+- **Type consistency:** no new TypeScript symbols; all CSS class names match between template and styles. Selectors used in tests match those rendered.
+- **Existing tests preserved:** confirmed via grep — `.chat-thread-list__grip` selector still finds the (now-nested) grip span.
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) {
+ ⋮⋮
+}
+
+ ...
+
+ @if (thread.pinned) {
+ ...
+ }
+ {{ threadLabel(thread) }}
+
+ ...
+
+```
+
+After:
+
+```html
+
+ ...
+
+ @if (thread.pinned) {
+
+ ...
+ @if (actions()?.reorderPinned) {
+ ⋮⋮
+ }
+
+ }
+ {{ threadLabel(thread) }}
+
+ ...
+
+```
+
+The ` ` wrapper, its `[attr.draggable]`, and the five drag handlers (`dragstart`, `dragover`, `dragleave`, `drop`, `dragend`) are unchanged. The grip is now a `` (not a ``), purely decorative, marked `aria-hidden` via its parent slot.
+
+## Styles
+
+Remove these existing rules:
+
+```css
+.chat-thread-list__grip {
+ flex-shrink: 0;
+ width: 16px;
+ height: 28px;
+ margin-right: 2px;
+ padding: 0;
+ border: 0;
+ background: transparent;
+ color: var(--ngaf-chat-text-muted);
+ cursor: grab;
+ opacity: 0;
+ transition: opacity 100ms ease;
+ font-size: 11px;
+ line-height: 1;
+ letter-spacing: -1px;
+ user-select: none;
+}
+.chat-thread-list__item-wrap:hover .chat-thread-list__grip,
+.chat-thread-list__item-wrap:focus-within .chat-thread-list__grip {
+ opacity: 1;
+}
+.chat-thread-list__grip:active { cursor: grabbing; }
+```
+
+Replace with:
+
+```css
+.chat-thread-list__pin-slot {
+ position: relative;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 13px;
+ height: 13px;
+ margin-right: 4px;
+ flex-shrink: 0;
+ vertical-align: -1px;
+}
+.chat-thread-list__pin-slot .chat-thread-list__item-pin {
+ position: absolute;
+ inset: 0;
+ width: 13px;
+ height: 13px;
+ opacity: 1;
+ transition: opacity 100ms ease;
+}
+.chat-thread-list__pin-slot .chat-thread-list__grip {
+ position: absolute;
+ inset: 0;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ color: var(--ngaf-chat-text-muted);
+ font-size: 11px;
+ line-height: 1;
+ letter-spacing: -1px;
+ opacity: 0;
+ transition: opacity 100ms ease;
+ user-select: none;
+ pointer-events: none;
+}
+.chat-thread-list__item-wrap:hover .chat-thread-list__pin-slot .chat-thread-list__item-pin,
+.chat-thread-list__item-wrap:focus-within .chat-thread-list__pin-slot .chat-thread-list__item-pin {
+ opacity: 0;
+}
+.chat-thread-list__item-wrap:hover .chat-thread-list__pin-slot .chat-thread-list__grip,
+.chat-thread-list__item-wrap:focus-within .chat-thread-list__pin-slot .chat-thread-list__grip {
+ opacity: 1;
+}
+.chat-thread-list__item-wrap[draggable="true"] { cursor: grab; }
+.chat-thread-list__item-wrap[draggable="true"]:active { cursor: grabbing; }
+```
+
+The wrap-level cursor change keys off the `draggable` attribute (which is `null` unless the row is pinned + has `reorderPinned`), so unpinned rows keep `cursor: pointer` from the item button.
+
+Note `pointer-events: none` on the grip glyph: prevents the decorative span from interfering with the underlying click target (the item button).
+
+## Error Handling
+
+None. Adding/removing the `reorderPinned` adapter method live, or pinning/unpinning a thread, simply changes whether the slot appears or whether the grip is rendered inside it. Angular's `@if` handles both.
+
+## Testing
+
+### Update three existing tests (`chat-thread-list.component.spec.ts`)
+
+1. **`grip renders for pinned thread when reorderPinned adapter is defined`** — change the selector from `.chat-thread-list__grip` (as a sibling) to `.chat-thread-list__pin-slot .chat-thread-list__grip`. Assert it's present.
+2. **`grip does not render when reorderPinned is undefined`** — same selector update. Assert the pin slot exists (`.chat-thread-list__pin-slot`) but the grip child does not.
+3. **`grip does not render for unpinned threads`** — selector update. Assert the pin slot itself is absent for unpinned rows.
+
+### Keep unchanged
+
+- The two drag-and-drop tests (`drop "before" target` and `drop "after" last pinned`) — they exercise the `` drag handlers, which are untouched.
+- The three menu-item tests (Move up / Move down) — untouched.
+- The reorder-rejection rollback test — untouched.
+
+### Manual verification
+
+Documented in the PR description:
+
+1. Open the chat example at `localhost:4200`.
+2. Pin one thread.
+3. Confirm: pinned row's left margin matches unpinned rows' (no gutter shift). Pin SVG visible in the title.
+4. Hover the pinned row. Confirm: pin SVG fades out, grip glyph (`⋮⋮`) fades in over the same slot. Cursor → `grab`.
+5. Pin a second thread, drag-reorder. Confirm: drag indicator works as before; drop succeeds.
+6. macOS Reduce Motion → on. Confirm: hover swap is instant (the universal `transition-duration: 0.01ms` applies).
+
+## Scope Boundaries
+
+**In scope:**
+- The two files listed
+- The three test updates above
+- The spec + plan + PR
+
+**Out of scope:**
+- Touch drag (still rejected — Move up / Move down menu items remain the keyboard/touch path)
+- Showing the grip on unpinned rows (no behavior change for unpinned)
+- Visual redesign of the pin icon itself
+- Changing the kebab Move-up / Move-down items
+
+## References
+
+- PR #280 — original drag-to-reorder implementation that introduced the gutter
+- `libs/chat/src/lib/primitives/chat-thread-list/chat-thread-list.component.ts` lines ~92–150 (template) and ~234–242 (state)
+- `libs/chat/src/lib/styles/chat-thread-list.styles.ts` lines ~126–155 (current grip rules)
diff --git a/libs/chat/src/lib/primitives/chat-thread-list/chat-thread-list.component.spec.ts b/libs/chat/src/lib/primitives/chat-thread-list/chat-thread-list.component.spec.ts
index b4599f9d..a1540f00 100644
--- a/libs/chat/src/lib/primitives/chat-thread-list/chat-thread-list.component.spec.ts
+++ b/libs/chat/src/lib/primitives/chat-thread-list/chat-thread-list.component.spec.ts
@@ -510,6 +510,32 @@ describe('ChatThreadListComponent', () => {
expect(fixture.nativeElement.querySelector('.chat-thread-list__grip')).toBeNull();
});
+ 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();
+ });
+
it('menu on pinned thread that is NOT first → includes "Move up"', () => {
const fixture = TestBed.createComponent(ChatThreadListComponent);
fixture.componentRef.setInput('threads', [
diff --git a/libs/chat/src/lib/primitives/chat-thread-list/chat-thread-list.component.ts b/libs/chat/src/lib/primitives/chat-thread-list/chat-thread-list.component.ts
index bbcfbcac..35d87160 100644
--- a/libs/chat/src/lib/primitives/chat-thread-list/chat-thread-list.component.ts
+++ b/libs/chat/src/lib/primitives/chat-thread-list/chat-thread-list.component.ts
@@ -119,14 +119,6 @@ export interface ThreadActionAdapter {
aria-label="Rename conversation"
/>
} @else {
- @if (thread.pinned && actions()?.reorderPinned) {
- ⋮⋮
- }
{{ initialOf(threadLabel(thread)) }}
@if (thread.pinned) {
-
-
-
+
+
+
+
+ @if (actions()?.reorderPinned) {
+ ⋮⋮
+ }
+
}
{{ threadLabel(thread) }}
diff --git a/libs/chat/src/lib/styles/chat-thread-list.styles.ts b/libs/chat/src/lib/styles/chat-thread-list.styles.ts
index 24815e81..79b3fe9c 100644
--- a/libs/chat/src/lib/styles/chat-thread-list.styles.ts
+++ b/libs/chat/src/lib/styles/chat-thread-list.styles.ts
@@ -123,28 +123,50 @@ export const CHAT_THREAD_LIST_STYLES = `
outline: none;
box-sizing: border-box;
}
- .chat-thread-list__grip {
+ .chat-thread-list__pin-slot {
+ position: relative;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 13px;
+ height: 13px;
+ margin-right: 4px;
flex-shrink: 0;
- width: 16px;
- height: 28px;
- margin-right: 2px;
- padding: 0;
- border: 0;
- background: transparent;
- color: var(--ngaf-chat-text-muted);
- cursor: grab;
- opacity: 0;
+ vertical-align: -1px;
+ }
+ .chat-thread-list__pin-slot .chat-thread-list__item-pin {
+ position: absolute;
+ inset: 0;
+ width: 13px;
+ height: 13px;
+ opacity: 1;
transition: opacity 100ms ease;
+ }
+ .chat-thread-list__pin-slot .chat-thread-list__grip {
+ position: absolute;
+ inset: 0;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ color: var(--ngaf-chat-text-muted);
font-size: 11px;
line-height: 1;
letter-spacing: -1px;
+ opacity: 0;
+ transition: opacity 100ms ease;
user-select: none;
+ pointer-events: none;
+ }
+ .chat-thread-list__item-wrap:hover .chat-thread-list__pin-slot .chat-thread-list__item-pin,
+ .chat-thread-list__item-wrap:focus-within .chat-thread-list__pin-slot .chat-thread-list__item-pin {
+ opacity: 0;
}
- .chat-thread-list__item-wrap:hover .chat-thread-list__grip,
- .chat-thread-list__item-wrap:focus-within .chat-thread-list__grip {
+ .chat-thread-list__item-wrap:hover .chat-thread-list__pin-slot .chat-thread-list__grip,
+ .chat-thread-list__item-wrap:focus-within .chat-thread-list__pin-slot .chat-thread-list__grip {
opacity: 1;
}
- .chat-thread-list__grip:active { cursor: grabbing; }
+ .chat-thread-list__item-wrap[draggable="true"] { cursor: grab; }
+ .chat-thread-list__item-wrap[draggable="true"]:active { cursor: grabbing; }
.chat-thread-list__item-wrap[data-dragging="true"] {
opacity: 0.4;