| 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,6 +253,37 @@ describe("sidebar collision detection", () => { |
| 253 | }); | 253 | }); |
| 254 | 254 | ||
| 255 | describe("sidebar drag projection", () => { | 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 | const pinned = [ | 287 | const pinned = [ |
| 257 | pinnedHeader, | 288 | pinnedHeader, |
| 258 | thread("p1", "pinned"), | 289 | thread("p1", "pinned"), |
apps/web/src/components/Sidebar.drag.ts+44-13| ... | @@ -194,27 +194,58 @@ export function createSidebarSortingStrategy(input: { | ... | @@ -194,27 +194,58 @@ export function createSidebarSortingStrategy(input: { |
| 194 | } | 194 | } |
| 195 | marker("settled-header"); | 195 | marker("settled-header"); |
| 196 | section("settled"); | 196 | section("settled"); |
| 197 | const result = items.map(() => hidden); | 197 | const heights = projected.map((item) => { |
| 198 | let top = rects[0].top; | ||
| 199 | for (const item of projected) { | ||
| 200 | const index = indices.get(sidebarListItemId(item)); | 198 | const index = indices.get(sidebarListItemId(item)); |
| 201 | const rect = index === undefined ? undefined : rects[index]; | 199 | const rect = index === undefined ? undefined : rects[index]; |
| 202 | if (index !== undefined && rect) result[index] = { ...stationary, y: top - rect.top }; | ||
| 203 | const fallback = | 200 | const fallback = |
| 204 | item.kind === "thread" && (item.section === "pinned" || item.section === "active") | 201 | item.kind === "thread" && (item.section === "pinned" || item.section === "active") |
| 205 | ? cardHeight | 202 | ? cardHeight |
| 206 | : slimHeight; | 203 | : slimHeight; |
| 207 | const moved = item.kind === "thread" && item.key === active.key; | 204 | const moved = item.kind === "thread" && item.key === active.key; |
| 208 | const height = | 205 | return item.kind === "marker" && |
| 209 | item.kind === "marker" && | ||
| 210 | (item.marker === "pinned-header" || item.marker === "pinned-divider") | 206 | (item.marker === "pinned-header" || item.marker === "pinned-divider") |
| 211 | ? labelHeight | 207 | ? labelHeight |
| 212 | : item.kind === "marker" && item.marker.endsWith("placeholder") | 208 | : item.kind === "marker" && item.marker.endsWith("placeholder") |
| 213 | ? slimHeight | 209 | ? slimHeight |
| 214 | : moved | 210 | : moved |
| 215 | ? fallback | 211 | ? fallback |
| 216 | : (rect?.height ?? fallback); | 212 | : (rect?.height ?? fallback); |
| 217 | top += height + 1; | 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 | result[activeIndex] = stationary; | 250 | result[activeIndex] = stationary; |
| 220 | return result; | 251 | return result; |
apps/web/src/components/Sidebar.tsx+18-4| ... | @@ -661,6 +661,7 @@ function SidebarDragBoundary(props: { | ... | @@ -661,6 +661,7 @@ function SidebarDragBoundary(props: { |
| 661 | function SidebarSectionHeader(props: { | 661 | function SidebarSectionHeader(props: { |
| 662 | marker: "snoozed-header" | "settled-header"; | 662 | marker: "snoozed-header" | "settled-header"; |
| 663 | label: string; | 663 | label: string; |
| 664 | className?: string; | ||
| 664 | // While dragging, the settled header reads at full strength and takes the | 665 | // While dragging, the settled header reads at full strength and takes the |
| 665 | // accent while the lifted row is over it. | 666 | // accent while the lifted row is over it. |
| 666 | dragging?: boolean; | 667 | dragging?: boolean; |
| ... | @@ -712,7 +713,7 @@ function SidebarSectionHeader(props: { | ... | @@ -712,7 +713,7 @@ function SidebarSectionHeader(props: { |
| 712 | <SortableSidebarMarker | 713 | <SortableSidebarMarker |
| 713 | marker={props.marker} | 714 | marker={props.marker} |
| 714 | data-testid={`sidebar-${props.marker}`} | 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 | <Tooltip disabled={!compact}> | 718 | <Tooltip disabled={!compact}> |
| 718 | <TooltipTrigger | 719 | <TooltipTrigger |
| ... | @@ -4541,7 +4542,10 @@ export default function Sidebar() { | ... | @@ -4541,7 +4542,10 @@ export default function Sidebar() { |
| 4541 | <> | 4542 | <> |
| 4542 | <SidebarChromeHeader isElectron={isElectron} /> | 4543 | <SidebarChromeHeader isElectron={isElectron} /> |
| 4543 | <SidebarContent | 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 | fixedFooter={ | 4549 | fixedFooter={ |
| 4546 | compact && !isSearchingThreads && snoozedThreads.length > 0 ? ( | 4550 | compact && !isSearchingThreads && snoozedThreads.length > 0 ? ( |
| 4547 | <ul | 4551 | <ul |
| ... | @@ -4717,7 +4721,12 @@ export default function Sidebar() { | ... | @@ -4717,7 +4721,12 @@ export default function Sidebar() { |
| 4717 | </SidebarGroup> | 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 | {isSearchingThreads ? ( | 4730 | {isSearchingThreads ? ( |
| 4722 | threadSearchResults.length > 0 ? ( | 4731 | threadSearchResults.length > 0 ? ( |
| 4723 | <TooltipProvider | 4732 | <TooltipProvider |
| ... | @@ -4806,7 +4815,10 @@ export default function Sidebar() { | ... | @@ -4806,7 +4815,10 @@ export default function Sidebar() { |
| 4806 | <ul | 4815 | <ul |
| 4807 | ref={attachListMotionRef} | 4816 | ref={attachListMotionRef} |
| 4808 | role="list" | 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 | const renderThreadRowInner = ( | 4824 | const renderThreadRowInner = ( |
| ... | @@ -5021,6 +5033,7 @@ export default function Sidebar() { | ... | @@ -5021,6 +5033,7 @@ export default function Sidebar() { |
| 5021 | <SidebarSectionHeader | 5033 | <SidebarSectionHeader |
| 5022 | key="snoozed-shelf-header" | 5034 | key="snoozed-shelf-header" |
| 5023 | marker="snoozed-header" | 5035 | marker="snoozed-header" |
| 5036 | className={cn(!compact && "mt-auto")} | ||
| 5024 | label={ | 5037 | label={ |
| 5025 | snoozedShelfExpanded | 5038 | snoozedShelfExpanded |
| 5026 | ? "Snoozed" | 5039 | ? "Snoozed" |
| ... | @@ -5038,6 +5051,7 @@ export default function Sidebar() { | ... | @@ -5038,6 +5051,7 @@ export default function Sidebar() { |
| 5038 | <SidebarSectionHeader | 5051 | <SidebarSectionHeader |
| 5039 | key="settled-shelf-header" | 5052 | key="settled-shelf-header" |
| 5040 | marker="settled-header" | 5053 | marker="settled-header" |
| 5054 | className={cn(!compact && snoozedThreads.length === 0 && "mt-auto")} | ||
| 5041 | label={ | 5055 | label={ |
| 5042 | settledShelfExpanded | 5056 | settledShelfExpanded |
| 5043 | ? "Settled" | 5057 | ? "Settled" |