From 5900d41e0abf6c4b20b8cfd36feea0d88c1faccb Mon Sep 17 00:00:00 2001 From: jeffvli Date: Sat, 4 Apr 2026 13:42:50 -0700 Subject: [PATCH] handle sticky elements on new layout --- .../use-item-table-sticky-layout-offsets.ts | 72 +++++++++++++++++++ .../hooks/use-sticky-table-group-rows.tsx | 21 +++--- .../hooks/use-sticky-table-header.tsx | 30 ++++---- .../item-table-list.module.css | 4 +- .../item-table-list/item-table-list.tsx | 5 ++ src/shared/styles/ag-grid.css | 43 ----------- src/shared/styles/global.css | 4 ++ 7 files changed, 104 insertions(+), 75 deletions(-) create mode 100644 src/renderer/components/item-list/item-table-list/hooks/use-item-table-sticky-layout-offsets.ts delete mode 100644 src/shared/styles/ag-grid.css diff --git a/src/renderer/components/item-list/item-table-list/hooks/use-item-table-sticky-layout-offsets.ts b/src/renderer/components/item-list/item-table-list/hooks/use-item-table-sticky-layout-offsets.ts new file mode 100644 index 000000000..77c825391 --- /dev/null +++ b/src/renderer/components/item-list/item-table-list/hooks/use-item-table-sticky-layout-offsets.ts @@ -0,0 +1,72 @@ +import { useLayoutEffect, useState } from 'react'; + +import { useWindowSettings } from '/@/renderer/store/settings.store'; +import { Platform } from '/@/shared/types/types'; + +export interface ItemTableStickyLayoutOffsets { + inViewMarginTop: number; + stickyTop: number; +} + +export function useItemTableStickyLayoutOffsets(): ItemTableStickyLayoutOffsets { + const { windowBarStyle } = useWindowSettings(); + const isWinMac = windowBarStyle === Platform.WINDOWS || windowBarStyle === Platform.MACOS; + + const [offsets, setOffsets] = useState(() => ({ + inViewMarginTop: getFallbackInViewMargin(windowBarStyle), + stickyTop: getFallbackStickyTop(windowBarStyle), + })); + + useLayoutEffect(() => { + const read = () => { + const topVar = isWinMac + ? '--item-table-sticky-top-win-mac' + : '--item-table-sticky-top-default'; + const marginVar = isWinMac + ? '--item-table-sticky-inview-margin-win-mac' + : '--item-table-sticky-inview-margin-default'; + setOffsets({ + inViewMarginTop: resolveRootCssMarginLeftVar( + marginVar, + getFallbackInViewMargin(windowBarStyle), + ), + stickyTop: resolveRootCssWidthVar(topVar, getFallbackStickyTop(windowBarStyle)), + }); + }; + + read(); + window.addEventListener('resize', read); + return () => window.removeEventListener('resize', read); + }, [isWinMac, windowBarStyle]); + + return offsets; +} + +function getFallbackInViewMargin(windowBarStyle: Platform): number { + return windowBarStyle === Platform.WINDOWS || windowBarStyle === Platform.MACOS ? -130 : -100; +} + +function getFallbackStickyTop(windowBarStyle: Platform): number { + return windowBarStyle === Platform.WINDOWS || windowBarStyle === Platform.MACOS ? 95 : 65; +} + +function resolveRootCssMarginLeftVar(varName: string, fallback: number): number { + if (typeof document === 'undefined') return fallback; + const el = document.createElement('div'); + el.style.cssText = `position:fixed;left:0;top:0;margin-left:var(${varName});width:1px;height:0;margin-top:0;margin-right:0;margin-bottom:0;padding:0;border:none;visibility:hidden;pointer-events:none;`; + document.body.appendChild(el); + const raw = getComputedStyle(el).marginLeft; + el.remove(); + const v = parseFloat(raw); + return Number.isFinite(v) ? v : fallback; +} + +function resolveRootCssWidthVar(varName: string, fallback: number): number { + if (typeof document === 'undefined') return fallback; + const el = document.createElement('div'); + el.style.cssText = `position:fixed;left:-99999px;top:0;width:var(${varName});height:0;margin:0;padding:0;border:none;visibility:hidden;pointer-events:none;`; + document.body.appendChild(el); + const w = el.getBoundingClientRect().width; + el.remove(); + return Number.isFinite(w) && w > 0 ? w : fallback; +} diff --git a/src/renderer/components/item-list/item-table-list/hooks/use-sticky-table-group-rows.tsx b/src/renderer/components/item-list/item-table-list/hooks/use-sticky-table-group-rows.tsx index e9ea39887..5699c95b3 100644 --- a/src/renderer/components/item-list/item-table-list/hooks/use-sticky-table-group-rows.tsx +++ b/src/renderer/components/item-list/item-table-list/hooks/use-sticky-table-group-rows.tsx @@ -1,9 +1,8 @@ +import type { ItemTableStickyLayoutOffsets } from '/@/renderer/components/item-list/item-table-list/hooks/use-item-table-sticky-layout-offsets'; + import { useInView } from 'motion/react'; import { useEffect, useMemo, useState } from 'react'; -import { useWindowSettings } from '/@/renderer/store/settings.store'; -import { Platform } from '/@/shared/types/types'; - export interface GroupRowInfo { groupIndex: number; rowIndex: number; @@ -18,6 +17,7 @@ export const useStickyTableGroupRows = ({ mainGridRef, shouldShowStickyHeader, stickyHeaderTop, + stickyLayout, }: { containerRef: React.RefObject; enabled: boolean; @@ -27,17 +27,14 @@ export const useStickyTableGroupRows = ({ mainGridRef: React.RefObject; shouldShowStickyHeader?: boolean; stickyHeaderTop?: number; + stickyLayout: ItemTableStickyLayoutOffsets; }) => { - const { windowBarStyle } = useWindowSettings(); + const { inViewMarginTop, stickyTop: layoutStickyTop } = stickyLayout; const [stickyGroupIndex, setStickyGroupIndex] = useState(null); - const topMargin = - windowBarStyle === Platform.WINDOWS || windowBarStyle === Platform.MACOS - ? '-130px' - : '-100px'; - + const groupRowsInViewMargin = `${inViewMarginTop}px 0px 0px 0px`; const isTableInView = useInView(containerRef, { - margin: `${topMargin} 0px 0px 0px`, + margin: groupRowsInViewMargin as NonNullable[1]>['margin'], }); const stickyTop = useMemo(() => { @@ -46,8 +43,8 @@ export const useStickyTableGroupRows = ({ if (shouldShowStickyHeader && stickyHeaderTop !== undefined) { return stickyHeaderTop + headerHeight + 1; } - return windowBarStyle === Platform.WINDOWS || windowBarStyle === Platform.MACOS ? 95 : 65; - }, [windowBarStyle, shouldShowStickyHeader, stickyHeaderTop, headerHeight]); + return layoutStickyTop; + }, [layoutStickyTop, shouldShowStickyHeader, stickyHeaderTop, headerHeight]); // Calculate group row indexes const groupRowIndexes = useMemo(() => { diff --git a/src/renderer/components/item-list/item-table-list/hooks/use-sticky-table-header.tsx b/src/renderer/components/item-list/item-table-list/hooks/use-sticky-table-header.tsx index d9073a292..4ad34bb44 100644 --- a/src/renderer/components/item-list/item-table-list/hooks/use-sticky-table-header.tsx +++ b/src/renderer/components/item-list/item-table-list/hooks/use-sticky-table-header.tsx @@ -1,9 +1,8 @@ +import type { ItemTableStickyLayoutOffsets } from '/@/renderer/components/item-list/item-table-list/hooks/use-item-table-sticky-layout-offsets'; + import { useInView } from 'motion/react'; import { RefObject, useEffect, useMemo, useRef } from 'react'; -import { useWindowSettings } from '/@/renderer/store/settings.store'; -import { Platform } from '/@/shared/types/types'; - export const useStickyTableHeader = ({ containerRef, enabled, @@ -12,6 +11,7 @@ export const useStickyTableHeader = ({ pinnedLeftColumnRef, pinnedRightColumnRef, stickyHeaderMainRef, + stickyLayout, }: { containerRef: RefObject; enabled: boolean; @@ -20,8 +20,9 @@ export const useStickyTableHeader = ({ pinnedLeftColumnRef?: RefObject; pinnedRightColumnRef?: RefObject; stickyHeaderMainRef?: RefObject; + stickyLayout: ItemTableStickyLayoutOffsets; }) => { - const { windowBarStyle } = useWindowSettings(); + const { inViewMarginTop, stickyTop } = stickyLayout; const isScrollingRef = useRef({ main: false, pinnedLeft: false, @@ -29,27 +30,20 @@ export const useStickyTableHeader = ({ stickyHeader: false, }); - const topMargin = - windowBarStyle === Platform.WINDOWS || windowBarStyle === Platform.MACOS - ? '-130px' - : '-100px'; + const inViewRootMargin = `${inViewMarginTop}px 0px 0px 0px`; - const isTableHeaderInView = useInView(headerRef, { - margin: `${topMargin} 0px 0px 0px`, - }); + const inViewOptions = { margin: inViewRootMargin } as { + margin: NonNullable[1]>['margin']; + }; - const isTableInView = useInView(containerRef, { - margin: `${topMargin} 0px 0px 0px`, - }); + const isTableHeaderInView = useInView(headerRef, inViewOptions); + + const isTableInView = useInView(containerRef, inViewOptions); const shouldShowStickyHeader = useMemo(() => { return enabled && !isTableHeaderInView && isTableInView; }, [enabled, isTableHeaderInView, isTableInView]); - const stickyTop = useMemo(() => { - return windowBarStyle === Platform.WINDOWS || windowBarStyle === Platform.MACOS ? 95 : 65; - }, [windowBarStyle]); - // Sync scroll between sticky header and main grid/pinned columns useEffect(() => { if (!shouldShowStickyHeader || !stickyHeaderMainRef?.current || !mainGridRef?.current) { diff --git a/src/renderer/components/item-list/item-table-list/item-table-list.module.css b/src/renderer/components/item-list/item-table-list/item-table-list.module.css index 802aba520..dc02a6882 100644 --- a/src/renderer/components/item-list/item-table-list/item-table-list.module.css +++ b/src/renderer/components/item-list/item-table-list/item-table-list.module.css @@ -52,7 +52,7 @@ .item-table-pinned-rows-grid-container.header-fixed { position: fixed !important; - top: 65px; + top: var(--item-table-sticky-top-default); z-index: 15; background-color: var(--theme-bg-primary); box-shadow: 0 -1px 0 0 var(--theme-colors-border); @@ -60,7 +60,7 @@ } .item-table-pinned-rows-grid-container.header-window-bar { - top: 95px; + top: var(--item-table-sticky-top-win-mac); } .item-table-list-container.header-fixed-margin { diff --git a/src/renderer/components/item-list/item-table-list/item-table-list.tsx b/src/renderer/components/item-list/item-table-list/item-table-list.tsx index cdcf953c6..e7677e9c7 100644 --- a/src/renderer/components/item-list/item-table-list/item-table-list.tsx +++ b/src/renderer/components/item-list/item-table-list/item-table-list.tsx @@ -31,6 +31,7 @@ import { parseTableColumns } from '/@/renderer/components/item-list/helpers/pars import { useListHotkeys } from '/@/renderer/components/item-list/helpers/use-list-hotkeys'; import { useContainerWidthTracking } from '/@/renderer/components/item-list/item-table-list/hooks/use-container-width-tracking'; import { useRowInteractionDelegate } from '/@/renderer/components/item-list/item-table-list/hooks/use-row-interaction-delegate'; +import { useItemTableStickyLayoutOffsets } from '/@/renderer/components/item-list/item-table-list/hooks/use-item-table-sticky-layout-offsets'; import { useStickyGroupRowPositioning } from '/@/renderer/components/item-list/item-table-list/hooks/use-sticky-group-row-positioning'; import { useStickyHeaderPositioning } from '/@/renderer/components/item-list/item-table-list/hooks/use-sticky-header-positioning'; import { useStickyTableGroupRows } from '/@/renderer/components/item-list/item-table-list/hooks/use-sticky-table-group-rows'; @@ -829,6 +830,8 @@ const ItemTableListStickyUI = memo( const stickyHeaderMainRef = useRef(null); const stickyHeaderRightRef = useRef(null); + const stickyLayout = useItemTableStickyLayoutOffsets(); + const { shouldShowStickyHeader, stickyTop } = useStickyTableHeader({ containerRef, enabled: enableHeader && enableStickyHeader, @@ -837,6 +840,7 @@ const ItemTableListStickyUI = memo( pinnedLeftColumnRef, pinnedRightColumnRef, stickyHeaderMainRef, + stickyLayout, }); useStickyHeaderPositioning({ @@ -858,6 +862,7 @@ const ItemTableListStickyUI = memo( mainGridRef: rowRef, shouldShowStickyHeader, stickyHeaderTop: stickyTop, + stickyLayout, }); const shouldRenderStickyGroupRow = shouldShowStickyGroupRow; diff --git a/src/shared/styles/ag-grid.css b/src/shared/styles/ag-grid.css deleted file mode 100644 index 7608f3467..000000000 --- a/src/shared/styles/ag-grid.css +++ /dev/null @@ -1,43 +0,0 @@ -.ag-header-fixed { - position: fixed !important; - top: 65px; - z-index: 15; - padding: 0 2rem; - margin: 0 -2rem; - box-shadow: 0 -1px 0 0 #181818; - transition: position 0.2s ease-in-out; -} - -.ag-header-window-bar { - top: 95px; -} - -.ag-header { - z-index: 5; -} - -.window-frame { - top: 95px; -} - -.ag-header-transparent { - --ag-header-background-color: rgb(0 0 0 / 0%) !important; -} - -.ag-header-fixed-margin { - margin-top: 36px !important; -} - -.ag-header-cell-comp-wrapper { - margin: 0 0.5rem; -} - -.ag-header-cell, -.ag-header-group-cell { - padding-right: 0.5rem; - padding-left: 0.5rem; -} - -.ag-header-cell-resize { - background-color: transparent; -} diff --git a/src/shared/styles/global.css b/src/shared/styles/global.css index f8ea044a6..4c42589b3 100644 --- a/src/shared/styles/global.css +++ b/src/shared/styles/global.css @@ -283,6 +283,10 @@ button { --theme-spacing-xs: var(--mantine-spacing-xs); --theme-spacing-sm: var(--mantine-spacing-sm); --theme-spacing-md: var(--mantine-spacing-md); + --item-table-sticky-top-win-mac: calc(95px + 2 * var(--theme-spacing-md)); + --item-table-sticky-top-default: calc(65px + var(--theme-spacing-md)); + --item-table-sticky-inview-margin-win-mac: calc(-130px - 2 * var(--theme-spacing-md)); + --item-table-sticky-inview-margin-default: calc(-100px - var(--theme-spacing-md)); --theme-spacing-lg: var(--mantine-spacing-lg); --theme-spacing-xl: var(--mantine-spacing-xl); --theme-spacing-2xl: var(--mantine-spacing-2xl);