mirror of
https://github.com/luxfi/explore.git
synced 2026-09-24 00:28:18 +00:00
Two mobile-QA defects across the block explorers.
Defect 1 (org explorers showed goerli stub data): api-explore.{zoo,hanzo,
pars}.network served the self-signed INGRESS DEFAULT CERT (no cert-manager
Ingress for those SNIs), so browsers rejected every FE fetch and react-query
fell back to the goerli stubs behind stuck skeletons. FE wiring + backend were
already correct (all 6 chains served real per-chain data via the existing
IngressRoutes). Fix: dedicated letsencrypt-prod Ingress per org api host ->
explorer-unified:8090, mirroring api-explore-lux-network. Certs issued, all
three now ssl_verify=0, real data flows. Also source per-org brand identity
(NETWORK_NAME/currency) in the configmaps so titles render the right brand.
Defect 2 (mobile 390px detail pages: dropped labels, horizontal clip, tab
overlap): three shared-UI fixes.
- Footer: was the real horizontal-overflow driver — inline gridTemplateColumns
minmax(auto,470px) never collapsed on mobile. Now grid-cols-1 on mobile.
- DetailedInfo.ItemValue: add min-w-0 + mobile wrap so long address/hash values
stop forcing the page wider than the viewport (desktop unchanged).
- AdaptiveTabsList: @luxfi/ui Tabs silently discard Chakra style props
(overflowX/w/flexShrink/...), so express mobile scroll + shrink-0 as
className. Tab strip now scrolls instead of collapsing/overlapping.
Co-authored-by: Hanzo Dev <[email protected]>
207 lines
6.6 KiB
TypeScript
207 lines
6.6 KiB
TypeScript
import { Skeleton } from '@luxfi/ui/skeleton';
|
|
import type { TabsProps } from '@luxfi/ui/tabs';
|
|
import { TabsCounter, TabsList, TabsTrigger } from '@luxfi/ui/tabs';
|
|
import React from 'react';
|
|
|
|
import type { TabItemRegular } from './types';
|
|
|
|
import useIsMobile from 'lib/hooks/useIsMobile';
|
|
import { cn } from 'lib/utils/cn';
|
|
|
|
import { useIsSticky } from '../../hooks/useIsSticky';
|
|
import AdaptiveTabsMenu from './AdaptiveTabsMenu';
|
|
import useAdaptiveTabs from './useAdaptiveTabs';
|
|
import useScrollToActiveTab from './useScrollToActiveTab';
|
|
import { menuButton, getTabValue } from './utils';
|
|
|
|
export interface SlotProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
widthAllocation?: 'available' | 'fixed';
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
[key: string]: any;
|
|
}
|
|
|
|
export interface BaseProps {
|
|
tabs: Array<TabItemRegular>;
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
listProps?: Record<string, any> | (({ isSticky, activeTab }: { isSticky: boolean; activeTab: string }) => Record<string, any>);
|
|
rightSlot?: React.ReactNode;
|
|
rightSlotProps?: SlotProps;
|
|
leftSlot?: React.ReactNode;
|
|
leftSlotProps?: SlotProps;
|
|
stickyEnabled?: boolean;
|
|
isLoading?: boolean;
|
|
}
|
|
|
|
interface Props extends BaseProps {
|
|
activeTab: string;
|
|
variant: TabsProps['variant'];
|
|
}
|
|
|
|
const HIDDEN_ITEM_STYLES: React.CSSProperties = {
|
|
position: 'absolute',
|
|
top: '-9999px',
|
|
left: '-9999px',
|
|
visibility: 'hidden',
|
|
};
|
|
|
|
const getItemStyles = (index: number, tabsCut: number | undefined, isLoading: boolean | undefined) => {
|
|
if (tabsCut === undefined || isLoading) {
|
|
return { style: HIDDEN_ITEM_STYLES } as never;
|
|
}
|
|
|
|
return index < tabsCut ? {} : { style: HIDDEN_ITEM_STYLES } as never;
|
|
};
|
|
|
|
const getMenuStyles = (tabsLength: number, tabsCut: number | undefined, isLoading: boolean | undefined) => {
|
|
if (tabsCut === undefined || isLoading) {
|
|
return { style: HIDDEN_ITEM_STYLES };
|
|
}
|
|
|
|
return tabsCut >= tabsLength ? { style: HIDDEN_ITEM_STYLES } : {};
|
|
};
|
|
|
|
const AdaptiveTabsList = (props: Props) => {
|
|
|
|
const {
|
|
tabs,
|
|
activeTab,
|
|
listProps,
|
|
rightSlot,
|
|
rightSlotProps,
|
|
leftSlot,
|
|
leftSlotProps,
|
|
stickyEnabled,
|
|
isLoading,
|
|
variant,
|
|
} = props;
|
|
|
|
const isMobile = useIsMobile();
|
|
|
|
const tabsList = React.useMemo(() => {
|
|
return [ ...tabs, menuButton ];
|
|
}, [ tabs ]);
|
|
|
|
const { tabsCut, tabsRefs, listRef, rightSlotRef, leftSlotRef } = useAdaptiveTabs(tabsList, isLoading || isMobile || tabs.length === 1);
|
|
const isSticky = useIsSticky(listRef, 5, stickyEnabled);
|
|
const activeTabIndex = tabsList.findIndex((tab) => getTabValue(tab) === activeTab) ?? 0;
|
|
useScrollToActiveTab({ activeTabIndex, listRef, tabsRefs, isMobile, isLoading });
|
|
|
|
if (tabs.length === 1 && !leftSlot && !rightSlot) {
|
|
return null;
|
|
}
|
|
|
|
const isReady = !isLoading && tabsCut !== undefined;
|
|
|
|
return (
|
|
<TabsList
|
|
ref={ listRef }
|
|
// @luxfi/ui TabsList discards Chakra style props (overflowX/w/mx/px/css…),
|
|
// so the mobile tab-bar behavior must be expressed as Tailwind classes —
|
|
// the only styling hook it forwards. Without this the bar can't scroll on
|
|
// mobile and the (non-shrinking) triggers collide past the 390px edge.
|
|
className={ cn(
|
|
'flex-nowrap items-center whitespace-nowrap bg-[var(--color-bg-primary)] mb-6',
|
|
// mobile: bleed to the page edges, horizontal scroll-snap, hidden scrollbar
|
|
'-mx-3 px-3 w-[calc(100%+24px)] overflow-x-auto overscroll-x-contain',
|
|
'snap-x snap-mandatory scroll-px-3 [scrollbar-width:none] [&::-webkit-scrollbar]:hidden',
|
|
// desktop: no bleed, no scroll — the adaptive "…" menu handles overflow
|
|
'lg:mx-0 lg:px-0 lg:w-full lg:overflow-x-visible lg:snap-none',
|
|
) }
|
|
{
|
|
...(props.stickyEnabled ? {
|
|
position: 'sticky',
|
|
boxShadow: { base: isSticky ? 'md' : 'none', lg: 'none' },
|
|
top: 0,
|
|
zIndex: { base: 'sticky2', lg: 'docked' },
|
|
} : { })
|
|
}
|
|
{
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
...(typeof listProps === 'function' ? listProps({ isSticky, activeTab }) : listProps) as any
|
|
}
|
|
>
|
|
{ leftSlot && (
|
|
<div
|
|
ref={ leftSlotRef }
|
|
{ ...leftSlotProps }
|
|
style={{
|
|
flexGrow: leftSlotProps?.widthAllocation === 'available' && tabsCut !== undefined ? 1 : undefined,
|
|
opacity: tabsCut !== undefined ? 1 : 0,
|
|
}}
|
|
>
|
|
{ leftSlot }
|
|
</div>
|
|
)
|
|
}
|
|
{ tabs.length > 1 && tabsList.map((tab, index) => {
|
|
const value = getTabValue(tab);
|
|
const ref = tabsRefs[index];
|
|
|
|
if (tab.id === 'menu') {
|
|
return (
|
|
<AdaptiveTabsMenu
|
|
key="menu"
|
|
ref={ ref }
|
|
tabs={ tabs }
|
|
tabsCut={ tabsCut ?? 0 }
|
|
isActive={ activeTabIndex > 0 && tabsCut !== undefined && tabsCut > 0 && activeTabIndex >= tabsCut }
|
|
{ ...(getMenuStyles(tabs.length, tabsCut, isLoading) as Record<string, unknown>) }
|
|
/>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<TabsTrigger
|
|
key={ value }
|
|
value={ value }
|
|
ref={ ref }
|
|
className="shrink-0 snap-start"
|
|
{ ...getItemStyles(index, tabsCut, isLoading) }
|
|
>
|
|
{ typeof tab.title === 'function' ? tab.title() : tab.title }
|
|
<TabsCounter count={ tab.count }/>
|
|
</TabsTrigger>
|
|
);
|
|
}) }
|
|
{ tabs.slice(0, isReady ? 0 : 5).map((tab, index) => {
|
|
const value = `${ getTabValue(tab) }-pre`;
|
|
return (
|
|
<TabsTrigger
|
|
key={ value }
|
|
value={ value }
|
|
className={ cn(
|
|
'shrink-0 snap-start',
|
|
activeTabIndex === index && (variant === 'solid' || variant === undefined) ?
|
|
'bg-black/5 dark:bg-white/5' :
|
|
undefined,
|
|
) }
|
|
>
|
|
<Skeleton loading>
|
|
{ typeof tab.title === 'function' ? tab.title() : tab.title }
|
|
<TabsCounter count={ tab.count }/>
|
|
</Skeleton>
|
|
</TabsTrigger>
|
|
);
|
|
}) }
|
|
{
|
|
rightSlot ? (
|
|
<div
|
|
ref={ rightSlotRef }
|
|
style={{
|
|
marginLeft: 'auto',
|
|
flexGrow: rightSlotProps?.widthAllocation === 'available' && tabsCut !== undefined ? 1 : undefined,
|
|
opacity: tabsCut !== undefined ? 1 : 0,
|
|
}}
|
|
{ ...rightSlotProps }
|
|
>
|
|
{ rightSlot }
|
|
</div>
|
|
) :
|
|
null
|
|
}
|
|
</TabsList>
|
|
);
|
|
};
|
|
|
|
export default React.memo(AdaptiveTabsList);
|