1- import {
2- CaretDownIcon ,
3- CaretRightIcon ,
4- CubeFocusIcon ,
5- PlusIcon ,
6- StarIcon ,
7- } from "@phosphor-icons/react" ;
8- import { Button , cn , MenuLabel } from "@posthog/quill" ;
1+ import { CaretRightIcon , PlusIcon , StarIcon } from "@phosphor-icons/react" ;
2+ import { Button , cn } from "@posthog/quill" ;
93import { ANALYTICS_EVENTS } from "@posthog/shared/analytics-events" ;
104import { CreateChannelModal } from "@posthog/ui/features/canvas/components/CreateChannelModal" ;
115import {
@@ -26,11 +20,12 @@ import { useNavigate, useRouterState } from "@tanstack/react-router";
2620import { useMemo , useState } from "react" ;
2721
2822/**
29- * The project's space directory. A section label (like Starred/Spaces in the
30- * channel list) that folds open to every shared space, alphabetically.
31- * Clicking a row opens the space; the hover star is what pins it into the
32- * sidebar above — pinned rows wear their star filled so the directory shows
33- * what's already up there. The personal space isn't listed: it can't be
23+ * The project's space directory, docked at the bottom of the sidebar. The
24+ * header is shaped exactly like a pinned space row (same caret, same inset) and
25+ * folding it open grows the section upward to a capped height, then the list
26+ * scrolls. Clicking a row opens the space; the hover star is what pins it into
27+ * the sidebar above — pinned rows wear their star filled so the directory
28+ * shows what's already up there. The personal space isn't listed: it can't be
3429 * pinned or shared, and it's always first in the pinned list anyway.
3530 */
3631export function AllSpacesSection ( ) {
@@ -82,89 +77,94 @@ export function AllSpacesSection() {
8277 } ;
8378
8479 return (
85- < div className = "flex flex-col gap-px px-2 pb-2" >
86- { /* Same header shape as the channel list's groups: MenuLabel carries the
87- sidebar's label styling, and the section glyph swaps to a disclosure
88- caret on hover or keyboard focus. */ }
89- < MenuLabel
90- render = {
91- < button
92- type = "button"
93- onClick = { toggle }
94- aria-expanded = { open }
95- className = "group/all-spaces flex w-full items-center gap-2"
96- />
97- }
80+ < div className = "shrink-0 border-border border-t" >
81+ < div
82+ className = { cn (
83+ "flex flex-col gap-px px-2 pt-1" ,
84+ // Open, the section ends in rows with hover controls at their right
85+ // edge — clear the floating create button. The bare header can share
86+ // its row with it.
87+ open ? "pb-12" : "pb-2" ,
88+ ) }
9889 >
99- < span className = "relative flex size-3.5 shrink-0 items-center justify-center" >
100- < span className = "group-hover/all-spaces:hidden group-focus-visible/all-spaces:hidden" >
101- < CubeFocusIcon size = { 14 } />
90+ { /* Same shape as a pinned space row, so the carets line up. */ }
91+ < Button
92+ variant = "default"
93+ left
94+ aria-expanded = { open }
95+ onClick = { toggle }
96+ className = "w-full gap-1.5 text-left"
97+ >
98+ < CaretRightIcon
99+ size = { 12 }
100+ className = { cn (
101+ "shrink-0 text-muted-foreground transition-transform" ,
102+ open && "rotate-90" ,
103+ ) }
104+ />
105+ < span className = "min-w-0 flex-1 truncate font-medium text-[13px] text-muted-foreground group-hover/button:text-foreground" >
106+ All spaces
102107 </ span >
103- { open ? (
104- < CaretDownIcon
105- size = { 14 }
106- className = "hidden group-hover/all-spaces:block group-focus-visible/all-spaces:block"
108+ </ Button >
109+
110+ { open && (
111+ < >
112+ < div className = "flex max-h-64 flex-col gap-px overflow-y-auto" >
113+ { spaces . map ( ( channel ) => {
114+ const shortcutId = starredRefToShortcutId . get ( channel . path ) ;
115+ const base = `/website/${ channel . id } ` ;
116+ const isActive =
117+ pathname === base || pathname . startsWith ( `${ base } /` ) ;
118+ return (
119+ // Overlay, not endContent: the row is a button already, and
120+ // a star nested inside it would be a button within a button.
121+ < div key = { channel . id } className = "group/space relative" >
122+ < SidebarItem
123+ depth = { 1 }
124+ label = { channel . name }
125+ isActive = { isActive }
126+ onClick = { ( ) => openSpace ( channel ) }
127+ // Star well, so the name truncates clear of the star.
128+ endContent = {
129+ < span aria-hidden className = "size-5 shrink-0" />
130+ }
131+ />
132+ < Button
133+ variant = "default"
134+ size = "icon-sm"
135+ aria-label = { shortcutId ? "Unpin space" : "Pin space" }
136+ onClick = { ( ) => togglePin ( channel ) }
137+ className = { cn (
138+ "-translate-y-1/2 absolute top-1/2 right-[2px] text-muted-foreground transition-opacity" ,
139+ shortcutId
140+ ? "opacity-100"
141+ : "opacity-0 focus-visible:opacity-100 group-hover/space:opacity-100" ,
142+ ) }
143+ >
144+ < StarIcon
145+ size = { 13 }
146+ weight = { shortcutId ? "fill" : "regular" }
147+ />
148+ </ Button >
149+ </ div >
150+ ) ;
151+ } ) }
152+ </ div >
153+ { /* Below the scroll region so it's reachable without scrolling
154+ the whole directory. */ }
155+ < SidebarItem
156+ depth = { 1 }
157+ icon = { < PlusIcon size = { 14 } className = "text-muted-foreground" /> }
158+ label = { < span className = "text-muted-foreground" > New space</ span > }
159+ onClick = { ( ) => setCreateOpen ( true ) }
107160 />
108- ) : (
109- < CaretRightIcon
110- size = { 14 }
111- className = "hidden group-hover/all-spaces:block group-focus-visible/all-spaces:block"
161+ < CreateChannelModal
162+ open = { createOpen }
163+ onOpenChange = { setCreateOpen }
112164 />
113- ) }
114- </ span >
115- All spaces
116- </ MenuLabel >
117-
118- { open && (
119- < >
120- { spaces . map ( ( channel ) => {
121- const shortcutId = starredRefToShortcutId . get ( channel . path ) ;
122- const base = `/website/${ channel . id } ` ;
123- const isActive =
124- pathname === base || pathname . startsWith ( `${ base } /` ) ;
125- return (
126- // Overlay, not endContent: the row is a button already, and a
127- // star nested inside it would be a button within a button.
128- < div key = { channel . id } className = "group/space relative" >
129- < SidebarItem
130- depth = { 1 }
131- label = { channel . name }
132- isActive = { isActive }
133- onClick = { ( ) => openSpace ( channel ) }
134- // Star well, so the name truncates clear of the hover star.
135- endContent = { < span aria-hidden className = "size-5 shrink-0" /> }
136- />
137- < Button
138- variant = "default"
139- size = "icon-sm"
140- aria-label = { shortcutId ? "Unpin space" : "Pin space" }
141- onClick = { ( ) => togglePin ( channel ) }
142- className = { cn (
143- "-translate-y-1/2 absolute top-1/2 right-[2px] text-muted-foreground transition-opacity" ,
144- shortcutId
145- ? "opacity-100"
146- : "opacity-0 focus-visible:opacity-100 group-hover/space:opacity-100" ,
147- ) }
148- >
149- < StarIcon
150- size = { 13 }
151- weight = { shortcutId ? "fill" : "regular" }
152- />
153- </ Button >
154- </ div >
155- ) ;
156- } ) }
157- { /* The directory is also where a space that doesn't exist yet would
158- be — so creating one starts here. */ }
159- < SidebarItem
160- depth = { 1 }
161- icon = { < PlusIcon size = { 14 } className = "text-muted-foreground" /> }
162- label = { < span className = "text-muted-foreground" > New space</ span > }
163- onClick = { ( ) => setCreateOpen ( true ) }
164- />
165- < CreateChannelModal open = { createOpen } onOpenChange = { setCreateOpen } />
166- </ >
167- ) }
165+ </ >
166+ ) }
167+ </ div >
168168 </ div >
169169 ) ;
170170}
0 commit comments