| author | |
| committer | |
| log | 0118b52295264a73ef7cd045437181f7b3e5a99f |
| tree | 28cff062c5f56b53799b772748e0aaad67ced7df |
| parent | f26198d799bb81bf754985d457a60f8a851ea895 |
| signature | Signed by PGP key B5690EEEBB952194 |
3 files changed, 93 insertions(+), 17 deletions(-)
apps/web/src/components/Sidebar.drag.test.ts+31| ... | ... | @@ -253,6 +253,37 @@ describe("sidebar collision detection", () => { |
| 253 | 253 | }); |
| 254 | 254 | |
| 255 | 255 | describe("sidebar drag projection", () => { |
| 256 | it.each([ | |
| 257 | ["a1", "a2"], | |
| 258 | ["a1", sidebarMarkerId("settled-header")], | |
| 259 | ["z", "a2"], | |
| 260 | ])("keeps sparse shelves at the bottom when dragging %s over %s", (active, over) => { | |
| 261 | const items = [ | |
| 262 | pinnedHeader, | |
| 263 | divider, | |
| 264 | thread("a1", "active"), | |
| 265 | thread("a2", "active"), | |
| 266 | marker("snoozed-header"), | |
| 267 | thread("z", "snoozed"), | |
| 268 | settledHeader, | |
| 269 | thread("s", "settled"), | |
| 270 | ]; | |
| 271 | const strategy = createSidebarSortingStrategy({ | |
| 272 | items, | |
| 273 | settledOrder: over === sidebarMarkerId("settled-header") ? ["a1", "s"] : ["s"], | |
| 274 | settledExpanded: true, | |
| 275 | boundaryLabelHeight: 24, | |
| 276 | snoozedThreadCount: 1, | |
| 277 | }); | |
| 278 | const args = layout(items, active, over); | |
| 279 | for (const rect of args.rects.slice(4)) { | |
| 280 | rect.top += 400; | |
| 281 | rect.bottom += 400; | |
| 282 | } | |
| 283 | const lastIndex = items.length - 1; | |
| 284 | expect(strategy({ ...args, index: lastIndex })).toEqual(stationary); | |
| 285 | }); | |
| 286 | ||
| 256 | 287 | const pinned = [ |
| 257 | 288 | pinnedHeader, |
| 258 | 289 | thread("p1", "pinned"), |
apps/web/src/components/Sidebar.drag.ts+44-13| ... | ... | @@ -194,27 +194,58 @@ export function createSidebarSortingStrategy(input: { |
| 194 | 194 | } |
| 195 | 195 | marker("settled-header"); |
| 196 | 196 | section("settled"); |
| 197 | const result = items.map(() => hidden); | |
| 198 | let top = rects[0].top; | |
| 199 | for (const item of projected) { | |
| 197 | const heights = projected.map((item) => { | |
| 200 | 198 | const index = indices.get(sidebarListItemId(item)); |
| 201 | 199 | const rect = index === undefined ? undefined : rects[index]; |
| 202 | if (index !== undefined && rect) result[index] = { ...stationary, y: top - rect.top }; | |
| 203 | 200 | const fallback = |
| 204 | 201 | item.kind === "thread" && (item.section === "pinned" || item.section === "active") |
| 205 | 202 | ? cardHeight |
| 206 | 203 | : slimHeight; |
| 207 | 204 | const moved = item.kind === "thread" && item.key === active.key; |
| 208 | const height = | |
| 209 | item.kind === "marker" && | |
| 205 | return item.kind === "marker" && | |
| 210 | 206 | (item.marker === "pinned-header" || item.marker === "pinned-divider") |
| 211 | ? labelHeight | |
| 212 | : item.kind === "marker" && item.marker.endsWith("placeholder") | |
| 213 | ? slimHeight | |
| 214 | : moved | |
| 215 | ? fallback | |
| 216 | : (rect?.height ?? fallback); | |
| 217 | top += height + 1; | |
| 207 | ? labelHeight | |
| 208 | : item.kind === "marker" && item.marker.endsWith("placeholder") | |
| 209 | ? slimHeight | |
| 210 | : moved | |
| 211 | ? fallback | |
| 212 | : (rect?.height ?? fallback); | |
| 213 | }); | |
| 214 | const firstShelf = items.findIndex( | |
| 215 | (item) => | |
| 216 | item.kind === "marker" && | |
| 217 | (item.marker === "snoozed-header" || item.marker === "settled-header"), | |
| 218 | ); | |
| 219 | const shelfRect = rects[firstShelf]; | |
| 220 | const beforeShelf = rects[firstShelf - 1]; | |
| 221 | const lastRect = rects.at(-1); | |
| 222 | // Consume the shelf's auto margin as drag labels and resized rows need | |
| 223 | // room, keeping the combined shelves at their measured bottom. | |
| 224 | let shelfSpace = | |
| 225 | !input.compact && | |
| 226 | shelfRect && | |
| 227 | beforeShelf && | |
| 228 | lastRect && | |
| 229 | shelfRect.top > beforeShelf.bottom + 1 | |
| 230 | ? Math.max( | |
| 231 | 0, | |
| 232 | lastRect.bottom - rects[0].top - heights.reduce((sum, height) => sum + height + 1, -1), | |
| 233 | ) | |
| 234 | : 0; | |
| 235 | const result = items.map(() => hidden); | |
| 236 | let top = rects[0].top; | |
| 237 | for (const [projectedIndex, item] of projected.entries()) { | |
| 238 | if ( | |
| 239 | item.kind === "marker" && | |
| 240 | (item.marker === "snoozed-header" || item.marker === "settled-header") | |
| 241 | ) { | |
| 242 | top += shelfSpace; | |
| 243 | shelfSpace = 0; | |
| 244 | } | |
| 245 | const index = indices.get(sidebarListItemId(item)); | |
| 246 | const rect = index === undefined ? undefined : rects[index]; | |
| 247 | if (index !== undefined && rect) result[index] = { ...stationary, y: top - rect.top }; | |
| 248 | top += heights[projectedIndex]! + 1; | |
| 218 | 249 | } |
| 219 | 250 | result[activeIndex] = stationary; |
| 220 | 251 | return result; |
apps/web/src/components/Sidebar.tsx+18-4| ... | ... | @@ -661,6 +661,7 @@ function SidebarDragBoundary(props: { |
| 661 | 661 | function SidebarSectionHeader(props: { |
| 662 | 662 | marker: "snoozed-header" | "settled-header"; |
| 663 | 663 | label: string; |
| 664 | className?: string; | |
| 664 | 665 | // While dragging, the settled header reads at full strength and takes the |
| 665 | 666 | // accent while the lifted row is over it. |
| 666 | 667 | dragging?: boolean; |
| ... | ... | @@ -712,7 +713,7 @@ function SidebarSectionHeader(props: { |
| 712 | 713 | <SortableSidebarMarker |
| 713 | 714 | marker={props.marker} |
| 714 | 715 | data-testid={`sidebar-${props.marker}`} |
| 715 | className="mx-0.5 h-8" | |
| 716 | className={cn("mx-0.5 h-8", props.className)} | |
| 716 | 717 | > |
| 717 | 718 | <Tooltip disabled={!compact}> |
| 718 | 719 | <TooltipTrigger |
| ... | ... | @@ -4541,7 +4542,10 @@ export default function Sidebar() { |
| 4541 | 4542 | <> |
| 4542 | 4543 | <SidebarChromeHeader isElectron={isElectron} /> |
| 4543 | 4544 | <SidebarContent |
| 4544 | className={cn("gap-0", compact && "group-data-[collapsible=icon]:overflow-visible")} | |
| 4545 | className={cn( | |
| 4546 | "gap-0", | |
| 4547 | compact ? "group-data-[collapsible=icon]:overflow-visible" : "min-h-full", | |
| 4548 | )} | |
| 4545 | 4549 | fixedFooter={ |
| 4546 | 4550 | compact && !isSearchingThreads && snoozedThreads.length > 0 ? ( |
| 4547 | 4551 | <ul |
| ... | ... | @@ -4717,7 +4721,12 @@ export default function Sidebar() { |
| 4717 | 4721 | </SidebarGroup> |
| 4718 | 4722 | } |
| 4719 | 4723 | > |
| 4720 | <SidebarGroup className="ps-[calc(var(--sidebar-content-inset)+1px)] pe-[var(--sidebar-content-inset)] pb-1 pt-0"> | |
| 4724 | <SidebarGroup | |
| 4725 | className={cn( | |
| 4726 | "ps-[calc(var(--sidebar-content-inset)+1px)] pe-[var(--sidebar-content-inset)] pb-1 pt-0", | |
| 4727 | !compact && "flex-1", | |
| 4728 | )} | |
| 4729 | > | |
| 4721 | 4730 | {isSearchingThreads ? ( |
| 4722 | 4731 | threadSearchResults.length > 0 ? ( |
| 4723 | 4732 | <TooltipProvider |
| ... | ... | @@ -4806,7 +4815,10 @@ export default function Sidebar() { |
| 4806 | 4815 | <ul |
| 4807 | 4816 | ref={attachListMotionRef} |
| 4808 | 4817 | role="list" |
| 4809 | className="relative flex flex-col gap-px" | |
| 4818 | className={cn( | |
| 4819 | "relative flex flex-col gap-px", | |
| 4820 | !compact && sidebarListItems.length > 0 && "flex-1", | |
| 4821 | )} | |
| 4810 | 4822 | > |
| 4811 | 4823 | {(() => { |
| 4812 | 4824 | const renderThreadRowInner = ( |
| ... | ... | @@ -5021,6 +5033,7 @@ export default function Sidebar() { |
| 5021 | 5033 | <SidebarSectionHeader |
| 5022 | 5034 | key="snoozed-shelf-header" |
| 5023 | 5035 | marker="snoozed-header" |
| 5036 | className={cn(!compact && "mt-auto")} | |
| 5024 | 5037 | label={ |
| 5025 | 5038 | snoozedShelfExpanded |
| 5026 | 5039 | ? "Snoozed" |
| ... | ... | @@ -5038,6 +5051,7 @@ export default function Sidebar() { |
| 5038 | 5051 | <SidebarSectionHeader |
| 5039 | 5052 | key="settled-shelf-header" |
| 5040 | 5053 | marker="settled-header" |
| 5054 | className={cn(!compact && snoozedThreads.length === 0 && "mt-auto")} | |
| 5041 | 5055 | label={ |
| 5042 | 5056 | settledShelfExpanded |
| 5043 | 5057 | ? "Settled" |