11import { For , Show , Switch , Match , createMemo } from "solid-js" ;
22import { config } from "../../stores/config" ;
3- import { viewState , ignoreItem , untrackItem , moveTrackedItem } from "../../stores/view" ;
3+ import { viewState , untrackItem , moveTrackedItem } from "../../stores/view" ;
4+ import type { TrackedItem } from "../../stores/view" ;
45import type { Issue , PullRequest } from "../../services/api" ;
56import ItemRow from "./ItemRow" ;
67import { Tooltip } from "../shared/Tooltip" ;
78
9+ function TypeBadge ( props : { type : TrackedItem [ "type" ] } ) {
10+ return (
11+ < Switch >
12+ < Match when = { props . type === "issue" } >
13+ < span class = "badge badge-outline badge-sm badge-info" > Issue</ span >
14+ </ Match >
15+ < Match when = { props . type === "pullRequest" } >
16+ < span class = "badge badge-outline badge-sm badge-success" > PR</ span >
17+ </ Match >
18+ </ Switch >
19+ ) ;
20+ }
21+
22+ // FLIP animation: record positions before move, animate slide after DOM updates
23+ const itemRefs = new Map < string , HTMLDivElement > ( ) ;
24+ const prefersReducedMotion = ( ) =>
25+ typeof window !== "undefined" && window . matchMedia ( "(prefers-reduced-motion: reduce)" ) . matches ;
26+
27+ function recordPositions ( ) : Map < string , DOMRect > {
28+ const snapshot = new Map < string , DOMRect > ( ) ;
29+ for ( const [ key , el ] of itemRefs ) {
30+ snapshot . set ( key , el . getBoundingClientRect ( ) ) ;
31+ }
32+ return snapshot ;
33+ }
34+
35+ function animateMove ( before : Map < string , DOMRect > ) {
36+ if ( prefersReducedMotion ( ) ) return ;
37+ requestAnimationFrame ( ( ) => {
38+ for ( const [ key , el ] of itemRefs ) {
39+ const old = before . get ( key ) ;
40+ if ( ! old ) continue ;
41+ const now = el . getBoundingClientRect ( ) ;
42+ const dy = old . top - now . top ;
43+ if ( Math . abs ( dy ) < 1 ) continue ;
44+ el . animate (
45+ [ { transform : `translateY(${ dy } px)` } , { transform : "translateY(0)" } ] ,
46+ { duration : 200 , easing : "ease-in-out" }
47+ ) ;
48+ }
49+ } ) ;
50+ }
51+
52+ function handleMove ( id : number , type : "issue" | "pullRequest" , direction : "up" | "down" ) {
53+ const before = recordPositions ( ) ;
54+ moveTrackedItem ( id , type , direction ) ;
55+ animateMove ( before ) ;
56+ }
57+
858export interface TrackedTabProps {
959 issues : Issue [ ] ;
1060 pullRequests : PullRequest [ ] ;
@@ -44,26 +94,36 @@ export default function TrackedTab(props: TrackedTabProps) {
4494
4595 const isFirst = ( ) => index ( ) === 0 ;
4696 const isLast = ( ) => index ( ) === viewState . trackedItems . length - 1 ;
97+ const itemKey = `${ item . type } :${ item . id } ` ;
4798
4899 return (
49- < div class = "flex items-center gap-1" >
50- { /* Arrow buttons */ }
100+ < div
101+ class = "flex items-center gap-1"
102+ ref = { ( el ) => { itemRefs . set ( itemKey , el ) ; } }
103+ >
104+ { /* Reorder buttons */ }
51105 < div class = "flex flex-col shrink-0 pl-2" >
52106 < button
53107 class = "btn btn-ghost btn-xs"
54108 disabled = { isFirst ( ) }
55109 aria-label = { `Move up: ${ item . title } ` }
56- onClick = { ( ) => moveTrackedItem ( item . id , item . type , "up" ) }
110+ onClick = { ( ) => handleMove ( item . id , item . type , "up" ) }
57111 >
58- ▲
112+ { /* Heroicons 20px solid: chevron-up */ }
113+ < svg xmlns = "http://www.w3.org/2000/svg" viewBox = "0 0 20 20" fill = "currentColor" class = "h-3.5 w-3.5" >
114+ < path fill-rule = "evenodd" d = "M14.77 12.79a.75.75 0 01-1.06-.02L10 8.832 6.29 12.77a.75.75 0 11-1.08-1.04l4.25-4.5a.75.75 0 011.08 0l4.25 4.5a.75.75 0 01-.02 1.06z" clip-rule = "evenodd" />
115+ </ svg >
59116 </ button >
60117 < button
61118 class = "btn btn-ghost btn-xs"
62119 disabled = { isLast ( ) }
63120 aria-label = { `Move down: ${ item . title } ` }
64- onClick = { ( ) => moveTrackedItem ( item . id , item . type , "down" ) }
121+ onClick = { ( ) => handleMove ( item . id , item . type , "down" ) }
65122 >
66- ▼
123+ { /* Heroicons 20px solid: chevron-down */ }
124+ < svg xmlns = "http://www.w3.org/2000/svg" viewBox = "0 0 20 20" fill = "currentColor" class = "h-3.5 w-3.5" >
125+ < path fill-rule = "evenodd" d = "M5.23 7.21a.75.75 0 011.06.02L10 11.168l3.71-3.938a.75.75 0 111.08 1.04l-4.25 4.5a.75.75 0 01-1.08 0l-4.25-4.5a.75.75 0 01.02-1.06z" clip-rule = "evenodd" />
126+ </ svg >
67127 </ button >
68128 </ div >
69129
@@ -79,14 +139,7 @@ export default function TrackedTab(props: TrackedTabProps) {
79139 < span class = "font-medium text-sm text-base-content truncate" >
80140 { item . title }
81141 </ span >
82- < Switch >
83- < Match when = { item . type === "issue" } >
84- < span class = "badge badge-outline badge-sm badge-info" > Issue</ span >
85- </ Match >
86- < Match when = { item . type === "pullRequest" } >
87- < span class = "badge badge-outline badge-sm badge-success" > PR</ span >
88- </ Match >
89- </ Switch >
142+ < TypeBadge type = { item . type } />
90143 </ div >
91144 < div class = "text-xs text-base-content/60 mt-0.5" >
92145 { item . repoFullName } { " " }
@@ -120,26 +173,9 @@ export default function TrackedTab(props: TrackedTabProps) {
120173 labels = { live ( ) . labels }
121174 onTrack = { ( ) => untrackItem ( item . id , item . type ) }
122175 isTracked = { true }
123- onIgnore = { ( ) => {
124- ignoreItem ( {
125- id : String ( item . id ) ,
126- type : item . type ,
127- repo : live ( ) . repoFullName ,
128- title : live ( ) . title ,
129- ignoredAt : Date . now ( ) ,
130- } ) ;
131- untrackItem ( item . id , item . type ) ;
132- } }
133176 density = { config . viewDensity }
134177 >
135- < Switch >
136- < Match when = { item . type === "issue" } >
137- < span class = "badge badge-outline badge-sm badge-info" > Issue</ span >
138- </ Match >
139- < Match when = { item . type === "pullRequest" } >
140- < span class = "badge badge-outline badge-sm badge-success" > PR</ span >
141- </ Match >
142- </ Switch >
178+ < TypeBadge type = { item . type } />
143179 </ ItemRow >
144180 ) }
145181 </ Show >
0 commit comments