diff --git a/apps/dashboard/src/components/compare/compare-page.tsx b/apps/dashboard/src/components/compare/compare-page.tsx index 88d37ba..04c9e13 100644 --- a/apps/dashboard/src/components/compare/compare-page.tsx +++ b/apps/dashboard/src/components/compare/compare-page.tsx @@ -3,10 +3,7 @@ import { toast } from "@diffkit/ui/components/sonner"; import { useQuery, useQueryClient } from "@tanstack/react-query"; import { useRouter } from "@tanstack/react-router"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; -import { - DetailPageSkeletonLayout, - StaggerItem, -} from "#/components/details/detail-page"; +import { DetailPageSkeletonLayout } from "#/components/details/detail-page"; import { createPullRequest } from "#/lib/github.functions"; import { type GitHubQueryScope, @@ -320,22 +317,16 @@ export function ComparePage({ function ComparePageSkeleton() { return ( - - -
-
-
-
- - -
-
-
-
- - -
- + +
+
+
+
+
+
+
+
+
); } diff --git a/apps/dashboard/src/components/details/detail-page.tsx b/apps/dashboard/src/components/details/detail-page.tsx index a88984f..5cd5281 100644 --- a/apps/dashboard/src/components/details/detail-page.tsx +++ b/apps/dashboard/src/components/details/detail-page.tsx @@ -1,110 +1,7 @@ import { Skeleton } from "@diffkit/ui/components/skeleton"; import { cn } from "@diffkit/ui/lib/utils"; import { Link } from "@tanstack/react-router"; -import { animate, motion, useMotionValue, useTransform } from "motion/react"; -import { createContext, useContext, useEffect, useState } from "react"; - -const STAGGER_DELAY = 1; -const ITEM_DURATION = 1.25; -const FADE_OUT_DURATION = 0.5; -const PAUSE_BEFORE_RESTART = 1; - -type StaggerContextValue = { - cycle: number; - groupOpacity: ReturnType>; -}; -const defaultGroupOpacity = { - get: () => 1, - set: () => {}, -} as unknown as ReturnType>; -const StaggerCycleContext = createContext({ - cycle: 0, - groupOpacity: defaultGroupOpacity, -}); - -function StaggerLoop({ - itemCount, - children, -}: { - itemCount: number; - children: React.ReactNode; -}) { - const [cycle, setCycle] = useState(0); - const groupOpacity = useMotionValue(1); - - // biome-ignore lint/correctness/useExhaustiveDependencies: cycle drives the restart loop - useEffect(() => { - const lastItemFinish = (itemCount - 1) * STAGGER_DELAY + ITEM_DURATION; - const totalVisible = lastItemFinish + PAUSE_BEFORE_RESTART; - - const timeout = setTimeout(() => { - const controls = animate(groupOpacity, 0, { - duration: FADE_OUT_DURATION, - ease: "easeInOut", - onComplete: () => { - groupOpacity.set(1); - setCycle((c) => c + 1); - }, - }); - return () => controls.stop(); - }, totalVisible * 1000); - - return () => clearTimeout(timeout); - }, [cycle, itemCount, groupOpacity]); - - return ( - - {children} - - ); -} - -function StaggerItem({ - children, - index, - className, -}: { - children: React.ReactNode; - index: number; - className?: string; -}) { - const { cycle, groupOpacity } = useContext(StaggerCycleContext); - const itemOpacity = useMotionValue(0); - const combinedOpacity = useTransform( - [itemOpacity, groupOpacity], - ([item, group]) => Math.min(item as number, group as number), - ); - - // biome-ignore lint/correctness/useExhaustiveDependencies: cycle resets the item animation - useEffect(() => { - itemOpacity.set(0); - const controls = animate(itemOpacity, 1, { - type: "spring", - duration: ITEM_DURATION, - bounce: 0, - delay: index * STAGGER_DELAY, - }); - return () => controls.stop(); - }, [cycle, index, itemOpacity]); - - return ( - - {children} - - ); -} +import type { ReactNode } from "react"; type DetailHeaderIcon = React.ComponentType<{ size?: number; @@ -112,12 +9,24 @@ type DetailHeaderIcon = React.ComponentType<{ className?: string; }>; +type DetailPageTitleProps = { + collectionHref: string; + collectionLabel: string; + owner: string; + repo: string; + number: number; + icon: DetailHeaderIcon; + iconClassName?: string; + title: string; + subtitle: ReactNode; +}; + export function DetailPageLayout({ main, sidebar, }: { - main: React.ReactNode; - sidebar: React.ReactNode; + main: ReactNode; + sidebar: ReactNode; }) { return (
@@ -139,17 +48,7 @@ export function DetailPageTitle({ iconClassName, title, subtitle, -}: { - collectionHref: string; - collectionLabel: string; - owner: string; - repo: string; - number: number; - icon: DetailHeaderIcon; - iconClassName?: string; - title: string; - subtitle: React.ReactNode; -}) { +}: DetailPageTitleProps) { return (
@@ -184,36 +83,30 @@ export function DetailPageTitle({ ); } -export { StaggerItem }; - export function DetailPageSkeletonLayout({ children, - mainItemCount, sidebarSectionCount = 3, }: { - children: React.ReactNode; - mainItemCount: number; + children: ReactNode; sidebarSectionCount?: number; }) { - const totalItems = Math.max(mainItemCount, sidebarSectionCount); return ( - -
-
-
{children}
-
- +
); } diff --git a/apps/dashboard/src/components/issues/detail/issue-detail-page.tsx b/apps/dashboard/src/components/issues/detail/issue-detail-page.tsx index 923646b..653a75e 100644 --- a/apps/dashboard/src/components/issues/detail/issue-detail-page.tsx +++ b/apps/dashboard/src/components/issues/detail/issue-detail-page.tsx @@ -5,7 +5,6 @@ import { useMemo } from "react"; import { DetailPageLayout, DetailPageSkeletonLayout, - StaggerItem, } from "#/components/details/detail-page"; import { githubIssuePageQueryOptions, @@ -154,77 +153,69 @@ export function IssueDetailContent({ function IssueDetailPageSkeleton() { return ( - - -
- -
- -
- -
- - -
+ +
+ +
+ +
+ +
+ +
- +
- -
- - -
-
+
+ + +
- -
-
- - - -
+
+
+ + +
- +
- -
-
- - -
-
- {/* Comment */} -
- -
- - -
-
- {/* Label event */} -
- - - -
- {/* Comment */} -
- -
- - -
+
+
+ + +
+
+ {/* Comment */} +
+ +
+ +
- {/* Assignment */} -
- - +
+ {/* Label event */} +
+ + + +
+ {/* Comment */} +
+ +
+ +
+ {/* Assignment */} +
+ + +
- +
); } diff --git a/apps/dashboard/src/components/layouts/dashboard-layout.tsx b/apps/dashboard/src/components/layouts/dashboard-layout.tsx index 92682e8..6928cc4 100644 --- a/apps/dashboard/src/components/layouts/dashboard-layout.tsx +++ b/apps/dashboard/src/components/layouts/dashboard-layout.tsx @@ -2,6 +2,7 @@ import { useQuery } from "@tanstack/react-query"; import { getRouteApi, Outlet } from "@tanstack/react-router"; import { motion } from "motion/react"; import { lazy, Suspense, useEffect } from "react"; +import { countUniqueById } from "#/lib/count-unique"; import { githubMyIssuesQueryOptions, githubMyPullsQueryOptions, @@ -68,17 +69,21 @@ export function DashboardLayout() { const pullCount = hasMounted && pullsQuery.data - ? pullsQuery.data.reviewRequested.length + - pullsQuery.data.assigned.length + - pullsQuery.data.authored.length + - pullsQuery.data.mentioned.length + - pullsQuery.data.involved.length + ? countUniqueById([ + ...pullsQuery.data.reviewRequested, + ...pullsQuery.data.assigned, + ...pullsQuery.data.authored, + ...pullsQuery.data.mentioned, + ...pullsQuery.data.involved, + ]) : undefined; const issueCount = hasMounted && issuesQuery.data - ? issuesQuery.data.assigned.length + - issuesQuery.data.authored.length + - issuesQuery.data.mentioned.length + ? countUniqueById([ + ...issuesQuery.data.assigned, + ...issuesQuery.data.authored, + ...issuesQuery.data.mentioned, + ]) : undefined; const tabsReady = hasMounted && Boolean(pullsQuery.data && issuesQuery.data); diff --git a/apps/dashboard/src/components/layouts/dashboard-side-panel.tsx b/apps/dashboard/src/components/layouts/dashboard-side-panel.tsx index bbe5bf4..3d2b638 100644 --- a/apps/dashboard/src/components/layouts/dashboard-side-panel.tsx +++ b/apps/dashboard/src/components/layouts/dashboard-side-panel.tsx @@ -9,9 +9,11 @@ import { useState, } from "react"; import { createPortal } from "react-dom"; +import { useLocalStorageState } from "#/lib/use-local-storage-state"; // w-72 (288px) + pl-2 (8px) export const SIDE_PANEL_WIDTH = 296; +const SIDE_PANEL_COLLAPSED_STORAGE_KEY = "diffkit:side-panel-collapsed"; type SidePanelState = { node: HTMLDivElement | null; @@ -27,11 +29,22 @@ const SidePanelContext = createContext({ toggle: () => {}, }); +function isBoolean(value: unknown): value is boolean { + return typeof value === "boolean"; +} + export function useSidePanelSlot() { const [node, setNode] = useState(null); - const [collapsed, setCollapsed] = useState(false); + const [collapsed, setCollapsed] = useLocalStorageState( + SIDE_PANEL_COLLAPSED_STORAGE_KEY, + { + defaultValue: false, + parse: (raw) => raw === "true", + validate: isBoolean, + }, + ); const [hasContent, setHasContent] = useState(false); - const toggle = useCallback(() => setCollapsed((c) => !c), []); + const toggle = useCallback(() => setCollapsed((c) => !c), [setCollapsed]); return { node, setNode, diff --git a/apps/dashboard/src/components/navigation/command-palette.tsx b/apps/dashboard/src/components/navigation/command-palette.tsx index 1a7b729..af200bf 100644 --- a/apps/dashboard/src/components/navigation/command-palette.tsx +++ b/apps/dashboard/src/components/navigation/command-palette.tsx @@ -90,7 +90,7 @@ export function CommandPalette() { value={search} onValueChange={setSearch} /> - + {getEmptyMessage( search, @@ -98,12 +98,17 @@ export function CommandPalette() { )} {Array.from(groups.entries()).map(([groupName, groupItems]) => ( - + {groupItems.map((item) => ( handleSelect(item)} + className="rounded-none !px-4" > {item.icon && ( - -
- -
- -
- -
- - -
+ +
+ +
+ +
+ +
+ +
- +
- -
- - - -
-
+
+ + + +
- -
-
- - - -
+
+
+ + +
- +
- -
-
- - -
-
- {/* Comment */} -
- -
- - -
-
- {/* Commit */} -
- - -
- {/* Review */} -
- -
- - -
+
+
+ + +
+
+ {/* Comment */} +
+ +
+ +
- {/* Label event */} -
- - - +
+ {/* Commit */} +
+ + +
+ {/* Review */} +
+ +
+ +
+ {/* Label event */} +
+ + + +
- +
- -
-
-
- - -
- +
+
+
+ +
+
- +
); } diff --git a/apps/dashboard/src/components/shared/sticky-group-header.tsx b/apps/dashboard/src/components/shared/sticky-group-header.tsx new file mode 100644 index 0000000..5f7729b --- /dev/null +++ b/apps/dashboard/src/components/shared/sticky-group-header.tsx @@ -0,0 +1,155 @@ +import { ChevronRightIcon } from "@diffkit/icons"; +import { cn } from "@diffkit/ui/lib/utils"; +import { AnimatePresence, motion } from "motion/react"; +import { + type ComponentType, + type ReactNode, + type RefObject, + useEffect, + useRef, + useState, +} from "react"; + +const ACCORDION_ANIMATION_ITEM_LIMIT = 5; + +export function StickyGroupHeader({ + sectionRef, + scrollContainerRef, + stickyTop: stickyTopOffset, + icon: Icon, + title, + count, + isEmpty, + isCollapsed, + onCollapsedChange, +}: { + sectionRef: RefObject; + scrollContainerRef: RefObject; + stickyTop: number; + icon: ComponentType<{ size?: number; strokeWidth?: number }>; + title: string; + count: number; + isEmpty: boolean; + isCollapsed: boolean; + onCollapsedChange: (isCollapsed: boolean) => void; +}) { + const headerRef = useRef(null); + const [isStickyActive, setIsStickyActive] = useState(false); + + useEffect(() => { + const scrollContainer = scrollContainerRef.current; + const section = sectionRef.current; + const header = headerRef.current; + + if (!scrollContainer || !section || !header) { + return; + } + + const updateStickyState = () => { + const scrollContainerRect = scrollContainer.getBoundingClientRect(); + const sectionRect = section.getBoundingClientRect(); + const stickyTop = scrollContainerRect.top + stickyTopOffset; + const headerHeight = header.offsetHeight; + const isStuck = + sectionRect.top <= stickyTop && + sectionRect.bottom > stickyTop + headerHeight; + + setIsStickyActive((current) => (current === isStuck ? current : isStuck)); + }; + + updateStickyState(); + scrollContainer.addEventListener("scroll", updateStickyState, { + passive: true, + }); + window.addEventListener("resize", updateStickyState); + + return () => { + scrollContainer.removeEventListener("scroll", updateStickyState); + window.removeEventListener("resize", updateStickyState); + }; + }, [scrollContainerRef, sectionRef, stickyTopOffset]); + + return ( + + ); +} + +export function StickyGroupContent({ + children, + isCollapsed, + itemCount, +}: { + children: ReactNode; + isCollapsed: boolean; + itemCount: number; +}) { + if (itemCount === 0) { + return null; + } + + const usesAccordionAnimation = itemCount < ACCORDION_ANIMATION_ITEM_LIMIT; + + if (usesAccordionAnimation) { + return ( + + {!isCollapsed && ( + + {children} + + )} + + ); + } + + return ( + + {!isCollapsed && ( + + {children} + + )} + + ); +} diff --git a/apps/dashboard/src/lib/collapsible-groups-storage.ts b/apps/dashboard/src/lib/collapsible-groups-storage.ts new file mode 100644 index 0000000..0012b2d --- /dev/null +++ b/apps/dashboard/src/lib/collapsible-groups-storage.ts @@ -0,0 +1,41 @@ +import { useCallback } from "react"; +import { useLocalStorageState } from "./use-local-storage-state"; + +type CollapsedGroupsState = Record; + +function isCollapsedGroupsState(value: unknown): value is CollapsedGroupsState { + return ( + typeof value === "object" && + value !== null && + !Array.isArray(value) && + Object.values(value).every((item) => typeof item === "boolean") + ); +} + +export function useCollapsedGroups(storageKey: string) { + const [collapsedGroups, setCollapsedGroups] = + useLocalStorageState(storageKey, { + defaultValue: {}, + parse: JSON.parse, + serialize: JSON.stringify, + validate: isCollapsedGroupsState, + }); + + const setGroupCollapsed = useCallback( + (groupId: string, isCollapsed: boolean) => { + setCollapsedGroups((currentGroups) => { + if (currentGroups[groupId] === isCollapsed) { + return currentGroups; + } + + return { + ...currentGroups, + [groupId]: isCollapsed, + }; + }); + }, + [setCollapsedGroups], + ); + + return { collapsedGroups, setGroupCollapsed }; +} diff --git a/apps/dashboard/src/lib/count-unique.ts b/apps/dashboard/src/lib/count-unique.ts new file mode 100644 index 0000000..bff6801 --- /dev/null +++ b/apps/dashboard/src/lib/count-unique.ts @@ -0,0 +1,3 @@ +export function countUniqueById(items: Iterable<{ id: number }>) { + return new Set(Array.from(items, (item) => item.id)).size; +} diff --git a/apps/dashboard/src/routes/_protected/issues.tsx b/apps/dashboard/src/routes/_protected/issues.tsx index a56483c..2d75950 100644 --- a/apps/dashboard/src/routes/_protected/issues.tsx +++ b/apps/dashboard/src/routes/_protected/issues.tsx @@ -1,15 +1,12 @@ import { CommentIcon, InboxIcon, IssuesIcon } from "@diffkit/icons"; -import { cn } from "@diffkit/ui/lib/utils"; import { useQuery } from "@tanstack/react-query"; import { createFileRoute } from "@tanstack/react-router"; import { type ComponentType, memo, type RefObject, - useEffect, useMemo, useRef, - useState, } from "react"; import { applyFilters, @@ -21,6 +18,12 @@ import { } from "#/components/filters"; import { IssueRow } from "#/components/issues/issue-row"; import { DashboardContentLoading } from "#/components/layouts/dashboard-content-loading"; +import { + StickyGroupContent, + StickyGroupHeader, +} from "#/components/shared/sticky-group-header"; +import { useCollapsedGroups } from "#/lib/collapsible-groups-storage"; +import { countUniqueById } from "#/lib/count-unique"; import { githubMyIssuesQueryOptions, githubQueryKeys, @@ -86,6 +89,9 @@ function IssuesPage() { defaultSortId: "updated", initialStore: filterStore, }); + const { collapsedGroups, setGroupCollapsed } = useCollapsedGroups( + ISSUES_GROUP_COLLAPSED_STORAGE_KEY, + ); if (query.error) throw query.error; if (query.data) { @@ -110,9 +116,8 @@ function IssuesPage() { issues: applyFilters(data.mentioned, filterState), }, ]; - const totalIssues = groups.reduce( - (sum, group) => sum + group.issues.length, - 0, + const totalIssues = countUniqueById( + groups.flatMap((group) => group.issues), ); return ( @@ -134,6 +139,7 @@ function IssuesPage() { href={`#${group.id}`} icon={group.icon} label={group.title} + onSelect={() => setGroupCollapsed(group.id, false)} value={group.issues.length} /> ))} @@ -149,6 +155,10 @@ function IssuesPage() { title={group.title} icon={group.icon} issues={group.issues} + isCollapsed={collapsedGroups[group.id] ?? false} + onCollapsedChange={(isCollapsed) => + setGroupCollapsed(group.id, isCollapsed) + } scrollContainerRef={scrollContainerRef} /> ))} @@ -168,16 +178,19 @@ type IssueGroupData = { }; const ISSUE_GROUP_STICKY_TOP = -32; +const ISSUES_GROUP_COLLAPSED_STORAGE_KEY = "diffkit:issues:collapsed-groups"; const IssueMetricCard = memo(function IssueMetricCard({ href, icon: Icon, label, + onSelect, value, }: { href: string; icon: ComponentType<{ size?: number; strokeWidth?: number }>; label: string; + onSelect: () => void; value: number; }) { const content = ( @@ -206,6 +219,7 @@ const IssueMetricCard = memo(function IssueMetricCard({ return ( {content} @@ -218,15 +232,21 @@ const IssueGroup = memo(function IssueGroup({ title, icon, issues, + isCollapsed, + onCollapsedChange, scrollContainerRef, }: { id: string; title: string; icon: ComponentType<{ size?: number; strokeWidth?: number }>; issues: IssueSummary[]; + isCollapsed: boolean; + onCollapsedChange: (isCollapsed: boolean) => void; scrollContainerRef: RefObject; }) { const sectionRef = useRef(null); + const hasIssues = issues.length > 0; + const isGroupCollapsed = hasIssues && isCollapsed; return (
- {issues.length > 0 && ( +
{issues.map((issue) => ( ))}
- )} +
); }); - -function StickyGroupHeader({ - sectionRef, - scrollContainerRef, - stickyTop: stickyTopOffset, - icon: Icon, - title, - count, - isEmpty, -}: { - sectionRef: RefObject; - scrollContainerRef: RefObject; - stickyTop: number; - icon: ComponentType<{ size?: number; strokeWidth?: number }>; - title: string; - count: number; - isEmpty: boolean; -}) { - const headerRef = useRef(null); - const [isStickyActive, setIsStickyActive] = useState(false); - - useEffect(() => { - const scrollContainer = scrollContainerRef.current; - const section = sectionRef.current; - const header = headerRef.current; - - if (!scrollContainer || !section || !header) { - return; - } - - const updateStickyState = () => { - const scrollContainerRect = scrollContainer.getBoundingClientRect(); - const sectionRect = section.getBoundingClientRect(); - const stickyTop = scrollContainerRect.top + stickyTopOffset; - const headerHeight = header.offsetHeight; - const isStuck = - sectionRect.top <= stickyTop && - sectionRect.bottom > stickyTop + headerHeight; - - setIsStickyActive((current) => (current === isStuck ? current : isStuck)); - }; - - updateStickyState(); - scrollContainer.addEventListener("scroll", updateStickyState, { - passive: true, - }); - window.addEventListener("resize", updateStickyState); - - return () => { - scrollContainer.removeEventListener("scroll", updateStickyState); - window.removeEventListener("resize", updateStickyState); - }; - }, [scrollContainerRef, sectionRef, stickyTopOffset]); - - return ( -
-
-
- -
-

{title}

-
- - {count} - -
- ); -} diff --git a/apps/dashboard/src/routes/_protected/pulls.tsx b/apps/dashboard/src/routes/_protected/pulls.tsx index 370496f..d2cb7b1 100644 --- a/apps/dashboard/src/routes/_protected/pulls.tsx +++ b/apps/dashboard/src/routes/_protected/pulls.tsx @@ -5,17 +5,14 @@ import { InboxIcon, ReviewsIcon, } from "@diffkit/icons"; -import { cn } from "@diffkit/ui/lib/utils"; import { useQuery } from "@tanstack/react-query"; import { createFileRoute } from "@tanstack/react-router"; import { type ComponentType, memo, type RefObject, - useEffect, useMemo, useRef, - useState, } from "react"; import { applyFilters, @@ -27,6 +24,12 @@ import { } from "#/components/filters"; import { DashboardContentLoading } from "#/components/layouts/dashboard-content-loading"; import { PullRequestRow } from "#/components/pulls/pull-request-row"; +import { + StickyGroupContent, + StickyGroupHeader, +} from "#/components/shared/sticky-group-header"; +import { useCollapsedGroups } from "#/lib/collapsible-groups-storage"; +import { countUniqueById } from "#/lib/count-unique"; import { githubMyPullsQueryOptions, githubQueryKeys } from "#/lib/github.query"; import type { PullSummary } from "#/lib/github.types"; import { githubRevalidationSignalKeys } from "#/lib/github-revalidation"; @@ -96,6 +99,9 @@ function PullRequestsPage() { defaultSortId: "updated", initialStore: filterStore, }); + const { collapsedGroups, setGroupCollapsed } = useCollapsedGroups( + PULLS_GROUP_COLLAPSED_STORAGE_KEY, + ); if (query.error) throw query.error; if (query.data) { @@ -132,10 +138,7 @@ function PullRequestsPage() { pulls: applyFilters(data.involved, filterState), }, ]; - const totalPulls = groups.reduce( - (sum, group) => sum + group.pulls.length, - 0, - ); + const totalPulls = countUniqueById(groups.flatMap((group) => group.pulls)); return (
@@ -161,6 +164,7 @@ function PullRequestsPage() { href={`#${group.id}`} icon={group.icon} label={group.title} + onSelect={() => setGroupCollapsed(group.id, false)} value={group.pulls.length} /> ))} @@ -176,6 +180,10 @@ function PullRequestsPage() { title={group.title} icon={group.icon} pulls={group.pulls} + isCollapsed={collapsedGroups[group.id] ?? false} + onCollapsedChange={(isCollapsed) => + setGroupCollapsed(group.id, isCollapsed) + } scope={scope} scrollContainerRef={scrollContainerRef} /> @@ -196,16 +204,19 @@ type PullGroupData = { }; const PULL_GROUP_STICKY_TOP = -32; +const PULLS_GROUP_COLLAPSED_STORAGE_KEY = "diffkit:pulls:collapsed-groups"; const PullMetricCard = memo(function PullMetricCard({ href, icon: Icon, label, + onSelect, value, }: { href: string; icon: ComponentType<{ size?: number; strokeWidth?: number }>; label: string; + onSelect: () => void; value: number; }) { const content = ( @@ -234,6 +245,7 @@ const PullMetricCard = memo(function PullMetricCard({ return ( {content} @@ -246,6 +258,8 @@ const PullGroup = memo(function PullGroup({ title, icon, pulls, + isCollapsed, + onCollapsedChange, scope, scrollContainerRef, }: { @@ -253,10 +267,14 @@ const PullGroup = memo(function PullGroup({ title: string; icon: ComponentType<{ size?: number; strokeWidth?: number }>; pulls: PullSummary[]; + isCollapsed: boolean; + onCollapsedChange: (isCollapsed: boolean) => void; scope: { userId: string }; scrollContainerRef: RefObject; }) { const sectionRef = useRef(null); + const hasPulls = pulls.length > 0; + const isGroupCollapsed = hasPulls && isCollapsed; return (
- {pulls.length > 0 && ( +
{pulls.map((pull) => ( ))}
- )} +
); }); - -function StickyGroupHeader({ - sectionRef, - scrollContainerRef, - stickyTop: stickyTopOffset, - icon: Icon, - title, - count, - isEmpty, -}: { - sectionRef: RefObject; - scrollContainerRef: RefObject; - stickyTop: number; - icon: ComponentType<{ size?: number; strokeWidth?: number }>; - title: string; - count: number; - isEmpty: boolean; -}) { - const headerRef = useRef(null); - const [isStickyActive, setIsStickyActive] = useState(false); - - useEffect(() => { - const scrollContainer = scrollContainerRef.current; - const section = sectionRef.current; - const header = headerRef.current; - - if (!scrollContainer || !section || !header) { - return; - } - - const updateStickyState = () => { - const scrollContainerRect = scrollContainer.getBoundingClientRect(); - const sectionRect = section.getBoundingClientRect(); - const stickyTop = scrollContainerRect.top + stickyTopOffset; - const headerHeight = header.offsetHeight; - const isStuck = - sectionRect.top <= stickyTop && - sectionRect.bottom > stickyTop + headerHeight; - - setIsStickyActive((current) => (current === isStuck ? current : isStuck)); - }; - - updateStickyState(); - scrollContainer.addEventListener("scroll", updateStickyState, { - passive: true, - }); - window.addEventListener("resize", updateStickyState); - - return () => { - scrollContainer.removeEventListener("scroll", updateStickyState); - window.removeEventListener("resize", updateStickyState); - }; - }, [scrollContainerRef, sectionRef, stickyTopOffset]); - - return ( -
-
-
- -
-

{title}

-
- - {count} - -
- ); -} diff --git a/package.json b/package.json index f4202a7..71de2fb 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,8 @@ "@pierre/diffs>@shikijs/transformers": "4.0.2" }, "patchedDependencies": { - "@pierre/diffs@1.1.12": "patches/@pierre__diffs@1.1.12.patch" + "@pierre/diffs@1.1.12": "patches/@pierre__diffs@1.1.12.patch", + "miniflare@4.20260409.0": "patches/miniflare@4.20260409.0.patch" } } } diff --git a/patches/miniflare@4.20260409.0.patch b/patches/miniflare@4.20260409.0.patch new file mode 100644 index 0000000..bda6594 --- /dev/null +++ b/patches/miniflare@4.20260409.0.patch @@ -0,0 +1,12 @@ +diff --git a/dist/src/index.js b/dist/src/index.js +index 3efc727987c0e73c67f11602cf0d2113d205353f..b4bc33dd5e95e00eccf0b26421f1c968bd048768 100644 +--- a/dist/src/index.js ++++ b/dist/src/index.js +@@ -87403,6 +87403,7 @@ var Miniflare = class { + } + return { + services: servicesArray, ++ v8Flags: ["--max-old-space-size=4096"], + sockets, + extensions, + structuredLogging: this.#structuredWorkerdLogs diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 58dd2a3..3005407 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -12,6 +12,9 @@ patchedDependencies: '@pierre/diffs@1.1.12': hash: 1c39efc1f6750a414b433733422874766964595227df55535d1c92a7cc27052f path: patches/@pierre__diffs@1.1.12.patch + miniflare@4.20260409.0: + hash: 33a04c242e76c6d970b59b4438429cc4fcedf77284b8d461f632da47fab39822 + path: patches/miniflare@4.20260409.0.patch importers: @@ -5502,7 +5505,7 @@ snapshots: '@cloudflare/vite-plugin@1.31.2(vite@7.3.2(@types/node@22.19.17)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.3))(workerd@1.20260409.1)(wrangler@4.81.1(@cloudflare/workers-types@4.20260413.1))': dependencies: '@cloudflare/unenv-preset': 2.16.0(unenv@2.0.0-rc.24)(workerd@1.20260409.1) - miniflare: 4.20260409.0 + miniflare: 4.20260409.0(patch_hash=33a04c242e76c6d970b59b4438429cc4fcedf77284b8d461f632da47fab39822) unenv: 2.0.0-rc.24 vite: 7.3.2(@types/node@22.19.17)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.3) wrangler: 4.81.1(@cloudflare/workers-types@4.20260413.1) @@ -9068,7 +9071,7 @@ snapshots: mimic-function@5.0.1: {} - miniflare@4.20260409.0: + miniflare@4.20260409.0(patch_hash=33a04c242e76c6d970b59b4438429cc4fcedf77284b8d461f632da47fab39822): dependencies: '@cspotcode/source-map-support': 0.8.1 sharp: 0.34.5 @@ -10005,7 +10008,7 @@ snapshots: '@cloudflare/unenv-preset': 2.16.0(unenv@2.0.0-rc.24)(workerd@1.20260409.1) blake3-wasm: 2.1.5 esbuild: 0.27.3 - miniflare: 4.20260409.0 + miniflare: 4.20260409.0(patch_hash=33a04c242e76c6d970b59b4438429cc4fcedf77284b8d461f632da47fab39822) path-to-regexp: 6.3.0 unenv: 2.0.0-rc.24 workerd: 1.20260409.1