Files
zeekayandHanzo Dev b2d18db7a3 fix(explore): un-stub org explorers (TLS) + mobile detail pages (v1.1.10)
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]>
2026-07-18 10:31:56 -07:00

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);