@@ -18,13 +18,21 @@ import { AllSpacesSection } from "@posthog/ui/features/canvas/components/AllSpac
1818import { ChannelNav } from "@posthog/ui/features/canvas/components/ChannelNav" ;
1919import { SpaceSection } from "@posthog/ui/features/canvas/components/SpaceSection" ;
2020import { WatchListSection } from "@posthog/ui/features/canvas/components/WatchListSection" ;
21- import type { Channel } from "@posthog/ui/features/canvas/hooks/useChannels" ;
21+ import {
22+ useChannelStarMutations ,
23+ useChannelStars ,
24+ } from "@posthog/ui/features/canvas/hooks/useChannelStars" ;
25+ import {
26+ type Channel ,
27+ useChannels ,
28+ } from "@posthog/ui/features/canvas/hooks/useChannels" ;
2229import { useStarredChannelSlots } from "@posthog/ui/features/canvas/hooks/useStarredChannelSlots" ;
2330import { PERSONAL_CHANNEL_NAME } from "@posthog/ui/features/canvas/hooks/useTaskChannels" ;
2431import { useSpacesSidebarStore } from "@posthog/ui/features/canvas/stores/spacesSidebarStore" ;
32+ import { toast } from "@posthog/ui/primitives/toast" ;
2533import { openTaskInput } from "@posthog/ui/router/useOpenTask" ;
2634import { track } from "@posthog/ui/shell/analytics" ;
27- import type { RefCallback } from "react" ;
35+ import type { DragEvent , RefCallback } from "react" ;
2836import { useMemo , useState } from "react" ;
2937
3038/**
@@ -81,6 +89,13 @@ export function SpacesSidebarNav() {
8189 ? searchText . trim ( ) . toLowerCase ( ) || undefined
8290 : undefined ;
8391
92+ // Dropping an All-spaces row into this region pins it (stars it), same as
93+ // the directory's star but as one gesture.
94+ const { channels } = useChannels ( ) ;
95+ const { starredRefToShortcutId } = useChannelStars ( ) ;
96+ const { star } = useChannelStarMutations ( ) ;
97+ const [ isSpaceDropTarget , setIsSpaceDropTarget ] = useState ( false ) ;
98+
8499 const me = pinnedSpaces . find ( ( c ) => c . name === PERSONAL_CHANNEL_NAME ) ;
85100 // The user's drag order over the starred set; spaces they've never dragged
86101 // keep their backend order after the ranked ones (sort is stable).
@@ -96,6 +111,41 @@ export function SpacesSidebarNav() {
96111 ) ;
97112 } , [ pinnedSpaces , spaceOrder ] ) ;
98113
114+ const handleSpaceDragOver = ( e : DragEvent ) => {
115+ if ( ! e . dataTransfer . types . includes ( "text/x-space-id" ) ) return ;
116+ e . preventDefault ( ) ;
117+ e . dataTransfer . dropEffect = "copy" ;
118+ setIsSpaceDropTarget ( true ) ;
119+ } ;
120+ const handleSpaceDragLeave = ( e : DragEvent ) => {
121+ // dragleave fires when crossing into children; only clear on a real exit.
122+ if ( e . currentTarget . contains ( e . relatedTarget as Node | null ) ) return ;
123+ setIsSpaceDropTarget ( false ) ;
124+ } ;
125+ const handleSpaceDrop = ( e : DragEvent ) => {
126+ setIsSpaceDropTarget ( false ) ;
127+ const spaceId = e . dataTransfer . getData ( "text/x-space-id" ) ;
128+ if ( ! spaceId ) return ;
129+ e . preventDefault ( ) ;
130+ const channel = channels . find ( ( c ) => c . id === spaceId ) ;
131+ if ( ! channel || starredRefToShortcutId . has ( channel . path ) ) return ;
132+ track ( ANALYTICS_EVENTS . CHANNEL_ACTION , {
133+ action_type : "star" ,
134+ surface : "sidebar" ,
135+ channel_id : channel . id ,
136+ } ) ;
137+ star ( channel ) . catch ( ( error : unknown ) =>
138+ toast . error ( "Couldn't pin space" , {
139+ description : error instanceof Error ? error . message : String ( error ) ,
140+ } ) ,
141+ ) ;
142+ // Pins land at the bottom of the user's order, ready to drag into place.
143+ setSpaceOrder ( [
144+ ...orderedSpaces . map ( ( c ) => c . id ) . filter ( ( id ) => id !== spaceId ) ,
145+ spaceId ,
146+ ] ) ;
147+ } ;
148+
99149 const handleDragEnd : DragDropEvents [ "dragend" ] = ( event ) => {
100150 if ( event . canceled ) return ;
101151 const sourceId = event . operation . source ?. id ;
@@ -201,8 +251,18 @@ export function SpacesSidebarNav() {
201251
202252 { /* Pinned (starred) spaces; #me first and fixed, the rest reorderable.
203253 This region is the sidebar's one scroll container — spaces unfold
204- their full lists inside it, so there are no nested scrollbars. */ }
205- < div className = "min-h-0 flex-1 overflow-y-auto px-2 pb-2" >
254+ their full lists inside it, so there are no nested scrollbars. It
255+ also accepts All-spaces rows dragged up from the directory below. */ }
256+ { /* biome-ignore lint/a11y/noStaticElementInteractions: drop target for space drags; the directory's star is the keyboard path */ }
257+ < div
258+ className = { cn (
259+ "min-h-0 flex-1 overflow-y-auto px-2 pb-2 transition-colors" ,
260+ isSpaceDropTarget && "bg-fill-hover" ,
261+ ) }
262+ onDragOver = { handleSpaceDragOver }
263+ onDragLeave = { handleSpaceDragLeave }
264+ onDrop = { handleSpaceDrop }
265+ >
206266 < div className = "flex flex-col gap-px" >
207267 { me && < SpaceSection channel = { me } query = { query } /> }
208268 { /* The handle doubles as the fold toggle, so a small pickup
0 commit comments