diff --git a/apps/docs/content/guides/auth/passwords.mdx b/apps/docs/content/guides/auth/passwords.mdx index d466c02e9e5..198c8e8f983 100644 --- a/apps/docs/content/guides/auth/passwords.mdx +++ b/apps/docs/content/guides/auth/passwords.mdx @@ -28,8 +28,6 @@ The instructions in this section assume that email confirmations are enabled. stickyTabList={{ style: { top: 'var(--header-height)', - backgroundColor: - 'oklch(from var(--background-default) l c h / var(--tw-bg-opacity))', maskImage: 'none', borderBottom: '1px solid oklch(from var(--border-default) l c h / 1)', } diff --git a/apps/docs/features/ui/Tabs.tsx b/apps/docs/features/ui/Tabs.tsx index f0f3f8529d8..7f839928083 100644 --- a/apps/docs/features/ui/Tabs.tsx +++ b/apps/docs/features/ui/Tabs.tsx @@ -1,43 +1,273 @@ 'use client' -import { useCallback, type ComponentPropsWithoutRef, type PropsWithChildren } from 'react' -import { Tabs as TabsPrimitive, type TabsProps } from 'ui' -import { withQueryParams, withSticky, type QueryParamsProps } from 'ui-patterns/ComplexTabs' -import { useTocRerenderTrigger } from '~/features/docs/GuidesMdx.state' +import { cva } from 'class-variance-authority' +import { Tabs as TabsPrimitive } from 'radix-ui' +import { + Children, + isValidElement, + ReactElement, + ReactNode, + useCallback, + useEffect, + useMemo, + useState, + type KeyboardEvent, +} from 'react' +import { cn } from 'ui' -const TabsWithStickyAndQueryParams = withSticky>( - withQueryParams(TabsPrimitive) +import { useTocRerenderTrigger } from '../docs/GuidesMdx.state' +import { useStickyTabs, UseStickyTabsOptions } from './useStickyTabs' +import { useTabsWithQueryParams, UseTabsWithQueryParamsOptions } from './useTabsWithQueryParams' + +export interface TabsProps { + children: ReactNode + type?: 'pills' | 'underlined' | 'cards' | 'rounded-pills' + defaultActiveId?: string + activeId?: string + size?: 'tiny' | 'small' | 'medium' | 'large' | 'xlarge' + block?: boolean + onChange?: (id: string) => void + onClick?: any + scrollable?: boolean + wrappable?: boolean + addOnBefore?: React.ReactNode + addOnAfter?: React.ReactNode + listClassNames?: string + baseClassNames?: string +} + +export const tabsListVariants = cva(cn('flex'), { + variants: { + type: { + pills: 'space-x-1', + underlined: 'items-center border-b border-secondary', + cards: '', + 'rounded-pills': 'flex-wrap gap-2', + }, + scrollable: { + true: 'overflow-auto whitespace-nowrap no-scrollbar mask-fadeout-right', + }, + wrappable: { + true: 'flex-wrap', + }, + }, +}) + +export const tabsTriggerListVariants = cva( + cn( + 'relative cursor-pointer flex items-center space-x-2 text-center transition focus:outline-hidden focus-visible:ring-3 focus-visible:ring-foreground-muted focus-visible:border-foreground-muted' + ), + { + variants: { + type: { + pills: 'shadow-xs rounded-sm border', + underlined: 'text-foreground-lighter', + cards: '', + 'rounded-pills': 'shadow-xs rounded-full', + }, + size: { + tiny: 'text-xs px-2.5 py-1', + small: 'text-base md:text-sm leading-4 px-3 py-2', + medium: 'text-base md:text-sm px-4 py-2', + large: 'text-base px-4 py-2', + xlarge: 'text-base px-6 py-3', + }, + block: { + true: 'w-full flex items-center justify-center', + }, + isActive: { + false: 'hover:text-foreground', + }, + }, + compoundVariants: [ + { + type: 'pills', + isActive: true, + className: 'bg-selection text-foreground border-stronger', + }, + { + type: 'pills', + isActive: false, + className: + 'bg-background border-strong hover:border-foreground-muted text-foreground-muted', + }, + { + type: 'underlined', + isActive: true, + className: '!text-foreground border-b-2 border-foreground', + }, + { + type: 'rounded-pills', + isActive: true, + className: 'bg-foreground text-background border-foreground', + }, + { + type: 'rounded-pills', + isActive: false, + className: + 'bg-surface-200 hover:bg-surface-300 hover:border-foreground-lighter text-foreground-lighter', + }, + ], + } ) -const TabPanel = TabsPrimitive.Panel -const Tabs = ({ +const isString = (maybeStr: unknown): maybeStr is string => typeof maybeStr === 'string' + +export const Tabs = ({ + defaultActiveId, + activeId, + type = 'pills', + size = 'tiny', + block, onChange, + onClick, + scrollable, + wrappable, + addOnBefore, + addOnAfter, + listClassNames, + baseClassNames, + children, + queryGroup, stickyTabList, - ...props -}: ComponentPropsWithoutRef) => { +}: TabsProps & + Pick & { + stickyTabList?: UseStickyTabsOptions + }) => { + const childrenArr: ReactElement[] = [] + const tabIds: string[] = [] + Children.forEach(children, (child) => { + if (isValidElement(child) && isString(child.props.id)) { + childrenArr.push(child) + tabIds.push(child.props.id) + } + }) + const { queryTab, onTabSelected: onTabSelectedForQuery } = useTabsWithQueryParams({ + tabIds, + queryGroup, + }) + + const sanitizedStickyTabList = useMemo( + () => + stickyTabList != undefined + ? { + ...stickyTabList, + // Magic number is the height of tab list + paragraph margin, worth getting + // rid of this? + scrollMarginTop: + stickyTabList.scrollMarginTop || 'calc(var(--header-height) + 43px + 20px)', + } + : undefined, + [stickyTabList] + ) + const { + observedRef, + stickyRef, + onTabSelected: onTabSelectedForSticky, + } = useStickyTabs(sanitizedStickyTabList) const rerenderToc = useTocRerenderTrigger() - const onChangeInternal = useCallback( - (...args: Parameters) => { - rerenderToc() - onChange?.(...args) - }, - [rerenderToc, onChange] + + const [activeTab, setActiveTab] = useState( + queryTab ?? + activeId ?? + defaultActiveId ?? + // if no defaultActiveId is set use the first panel + childrenArr?.[0]?.props?.id ) - if (stickyTabList && !stickyTabList.scrollMarginTop) { - // Magic number is the height of tab list + paragraph margin, worth getting - // rid of this? - stickyTabList.scrollMarginTop = 'calc(var(--header-height) + 43px + 20px)' - } + useEffect(() => { + // If we have a queryTab, Tabs is controller by URL params + if (queryTab && queryTab !== activeTab) { + setActiveTab(queryTab) + return + } + if (activeId && activeId !== activeTab) { + setActiveTab(activeId) + } + }, [activeId, activeTab, queryTab]) + const onTabClick = useCallback( + (id: string) => { + if (id !== activeTab) { + setActiveTab(id) + onTabSelectedForSticky() + onTabSelectedForQuery(id) + rerenderToc() + onChange?.(id) + onClick?.(id) + } + }, + [ + activeTab, + onTabSelectedForSticky, + onTabSelectedForQuery, + rerenderToc, + onChange, + onClick, + setActiveTab, + ] + ) return ( - + + + {addOnBefore} + {childrenArr.map((tab) => { + const isActive = activeTab === tab.props.id + + return ( + ) => { + if (e.key === 'Enter') { + e.preventDefault() + onTabClick(tab.props.id) + } + }} + onClick={() => onTabClick(tab.props.id)} + key={`${tab.props.id}-tab-button`} + value={tab.props.id} + className={tabsTriggerListVariants({ type, isActive, size, block })} + > + {tab.props.icon} + {tab.props.label} + {tab.props.iconRight} + + ) + })} + {addOnAfter} + + {childrenArr} + ) } -export { TabPanel, Tabs } +interface TabPanelProps { + children: ReactNode + id: string + label?: string + icon?: React.ReactNode + iconRight?: React.ReactNode + className?: string +} + +export const TabPanel = ({ children, id, className }: TabPanelProps) => { + return ( + + {children} + + ) +} diff --git a/apps/docs/features/ui/useStickyTabs.tsx b/apps/docs/features/ui/useStickyTabs.tsx new file mode 100644 index 00000000000..d657271a9aa --- /dev/null +++ b/apps/docs/features/ui/useStickyTabs.tsx @@ -0,0 +1,45 @@ +'use client' + +import { useCallback, useMemo, type FC } from 'react' + +import { useSticky } from './useStickyTabs.utils' + +export type UseStickyTabsOptions = { scrollMarginTop?: string; style?: CSSStyleDeclaration } + +export const useStickyTabs = (options?: UseStickyTabsOptions) => { + const enabled = !!options + const { scrollMarginTop, style } = options || {} + const { inView, observedRef, stickyRef } = useSticky({ + enabled, + style, + }) + + const onTabSelected = useCallback(() => { + if (enabled && inView && stickyRef.current) { + let elem = stickyRef.current as Element | null + while (elem && !elem.matches('[role="tabpanel"][data-state="active"]')) { + elem = elem.nextElementSibling + } + if (!elem) return + + const top = elem.getBoundingClientRect().top + ;(elem as HTMLElement).style.scrollMarginTop = scrollMarginTop || '0px' + if (top < 0) { + elem.scrollIntoView({ + behavior: window.matchMedia('(prefers-reduced-motion: no-preference)').matches + ? 'smooth' + : 'instant', + }) + } + } + }, [enabled, inView, scrollMarginTop, stickyRef]) + + return useMemo( + () => ({ + observedRef, + stickyRef, + onTabSelected, + }), + [observedRef, stickyRef, onTabSelected] + ) +} diff --git a/packages/ui-patterns/src/ComplexTabs/withSticky.utils.ts b/apps/docs/features/ui/useStickyTabs.utils.ts similarity index 100% rename from packages/ui-patterns/src/ComplexTabs/withSticky.utils.ts rename to apps/docs/features/ui/useStickyTabs.utils.ts diff --git a/apps/docs/features/ui/useTabsWithQueryParams.tsx b/apps/docs/features/ui/useTabsWithQueryParams.tsx new file mode 100644 index 00000000000..8d45d0dff17 --- /dev/null +++ b/apps/docs/features/ui/useTabsWithQueryParams.tsx @@ -0,0 +1,103 @@ +'use client' + +import { useSearchParamsShallow } from 'common' +import { xor } from 'lodash-es' +import { useCallback, useEffect, useMemo, useRef } from 'react' + +const LOCAL_STORAGE_KEY = 'supabase.ui-patterns.ComplexTabs.withQueryParams.v0' + +export interface UseTabsWithQueryParamsOptions { + tabIds: string[] + queryGroup?: string +} + +/** + * Wraps the basic `Tabs` component from the `ui` library so it stores + * selection state in query params. + */ +export const useTabsWithQueryParams = ({ tabIds, queryGroup }: UseTabsWithQueryParamsOptions) => { + // Store in ref to avoid stale data in later timeout + const tabIdsRef = useRef(tabIds) + tabIdsRef.current = tabIds + + // Store in ref to avoid stale data in later timeout + const queryGroupRef = useRef(queryGroup) + queryGroupRef.current = queryGroup + + const searchParams = useSearchParamsShallow() + const queryTabMaybe = queryGroupRef.current && searchParams.get(queryGroupRef.current) + const queryTab = + queryTabMaybe && tabIdsRef.current.includes(queryTabMaybe) ? queryTabMaybe : undefined + + const onTabSelected = useCallback( + (id: string) => { + if (queryGroupRef.current) { + if (!searchParams.getAll('queryGroups').includes(queryGroupRef.current)) { + searchParams.append('queryGroups', queryGroupRef.current) + } + searchParams.set(queryGroupRef.current, id) + } + }, + [searchParams] + ) + + const checkedLocalStorage = useRef(false) + useEffect(() => { + let timeout: number | undefined + if (!checkedLocalStorage.current) { + // Timeout to avoid something (I think the router) overwriting it + timeout = window.setTimeout(() => { + if ( + queryGroupRef.current && + !new URLSearchParams(window.location.search).has(queryGroupRef.current) + ) { + try { + const storedValues = JSON.parse(localStorage.getItem(LOCAL_STORAGE_KEY) ?? '') + if (storedValues === null || typeof storedValues !== 'object') return + + let storedValue: any = null + let maxDiff = tabIdsRef.current.length + Object.entries(storedValues).forEach(([key, value]) => { + const arr = key.split(',') + const diff = xor(arr, tabIdsRef.current) + if (diff.length < maxDiff) { + maxDiff = diff.length + storedValue = value + } + }) + + if (storedValue && tabIdsRef.current.includes(storedValue)) { + onTabSelected(storedValue) + } + } catch { + // ignore + } + } + }, 300) + + checkedLocalStorage.current = true + } + + if (queryGroupRef.current && queryTab) { + let updatedValues: Record = {} + try { + const oldValues = JSON.parse(localStorage.getItem(LOCAL_STORAGE_KEY) ?? '') + if (oldValues && typeof oldValues === 'object') { + updatedValues = oldValues + } + } catch { + // ignore + } + + updatedValues[tabIdsRef.current.sort().join(',')] = queryTab + + try { + localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(updatedValues)) + } catch { + // ignore + } + } + }, [queryTab, onTabSelected]) + + return useMemo(() => ({ queryTab, onTabSelected }), [queryTab, onTabSelected]) +} diff --git a/apps/docs/package.json b/apps/docs/package.json index 00c4c4be33c..c82cd4ca9f1 100644 --- a/apps/docs/package.json +++ b/apps/docs/package.json @@ -72,6 +72,7 @@ "acorn-typescript": "^1.4.13", "ai-commands": "workspace:*", "beautiful-mermaid": "^1.1.3", + "class-variance-authority": "^0.7.1", "common": "workspace:*", "common-tags": "^1.8.2", "config": "workspace:*", diff --git a/packages/ui-patterns/package.json b/packages/ui-patterns/package.json index 6ffff31a805..a4c164bd59e 100644 --- a/packages/ui-patterns/package.json +++ b/packages/ui-patterns/package.json @@ -250,22 +250,6 @@ "import": "./src/CommandMenu/prepackaged/shared/constants.ts", "types": "./src/CommandMenu/prepackaged/shared/constants.ts" }, - "./ComplexTabs": { - "import": "./src/ComplexTabs/index.tsx", - "types": "./src/ComplexTabs/index.tsx" - }, - "./ComplexTabs/withQueryParams": { - "import": "./src/ComplexTabs/withQueryParams.tsx", - "types": "./src/ComplexTabs/withQueryParams.tsx" - }, - "./ComplexTabs/withSticky": { - "import": "./src/ComplexTabs/withSticky.tsx", - "types": "./src/ComplexTabs/withSticky.tsx" - }, - "./ComplexTabs/withSticky.utils": { - "import": "./src/ComplexTabs/withSticky.utils.ts", - "types": "./src/ComplexTabs/withSticky.utils.ts" - }, "./ComputeBadge": { "import": "./src/ComputeBadge/index.tsx", "types": "./src/ComputeBadge/index.tsx" diff --git a/packages/ui-patterns/src/ComplexTabs/index.tsx b/packages/ui-patterns/src/ComplexTabs/index.tsx deleted file mode 100644 index 2c25bab1b10..00000000000 --- a/packages/ui-patterns/src/ComplexTabs/index.tsx +++ /dev/null @@ -1,2 +0,0 @@ -export * from './withQueryParams' -export * from './withSticky' diff --git a/packages/ui-patterns/src/ComplexTabs/withQueryParams.tsx b/packages/ui-patterns/src/ComplexTabs/withQueryParams.tsx deleted file mode 100644 index 6645f3653f1..00000000000 --- a/packages/ui-patterns/src/ComplexTabs/withQueryParams.tsx +++ /dev/null @@ -1,131 +0,0 @@ -'use client' - -import { useSearchParamsShallow } from 'common' -import { xor } from 'lodash' -import { Children, isValidElement, useEffect, useRef, type FC, type PropsWithChildren } from 'react' -import { type TabsProps } from 'ui' - -const isString = (maybeStr: unknown): maybeStr is string => typeof maybeStr === 'string' - -const LOCAL_STORAGE_KEY = 'supabase.ui-patterns.ComplexTabs.withQueryParams.v0' - -interface QueryParamsProps { - queryGroup?: string -} - -/** - * Wraps the basic `Tabs` component from the `ui` library so it stores - * selection state in query params. - */ -const withQueryParams = - >( - Component: FC> - ) => - ({ - children: childrenUnvalidated, - queryGroup: queryGroupTemp, - onClick, - ...props - }: Props & QueryParamsProps) => { - // Avoid Children.toArray — it clones elements (accessing element.ref) which - // triggers a React 19 warning. Children.forEach iterates without cloning. - const tabIdsTemp: string[] = [] - Children.forEach(childrenUnvalidated, (child) => { - if (isValidElement(child) && isString((child.props as any).id)) { - tabIdsTemp.push((child.props as any).id) - } - }) - // Store in ref to avoid stale data in later timeout - const tabIdsRef = useRef(tabIdsTemp) - tabIdsRef.current = tabIdsTemp - - // Store in ref to avoid stale data in later timeout - const queryGroupRef = useRef(queryGroupTemp) - queryGroupRef.current = queryGroupTemp - - const searchParams = useSearchParamsShallow() - const queryTabMaybe = queryGroupRef.current && searchParams.get(queryGroupRef.current) - const queryTab = - queryTabMaybe && tabIdsRef.current.includes(queryTabMaybe) ? queryTabMaybe : undefined - - const checkedLocalStorage = useRef(false) - useEffect(() => { - if (!checkedLocalStorage.current) { - // Timeout to avoid something (I think the router) overwriting it - setTimeout(() => { - if ( - queryGroupRef.current && - !new URLSearchParams(window.location.search).has(queryGroupRef.current) - ) { - try { - const storedValues = JSON.parse(localStorage.getItem(LOCAL_STORAGE_KEY) ?? '') - if (storedValues === null || typeof storedValues !== 'object') return - - let storedValue: any = null - let maxDiff = tabIdsRef.current.length - Object.entries(storedValues).forEach(([key, value]) => { - const arr = key.split(',') - const diff = xor(arr, tabIdsRef.current) - if (diff.length < maxDiff) { - maxDiff = diff.length - storedValue = value - } - }) - - if (storedValue && tabIdsRef.current.includes(storedValue)) { - switchTab(storedValue) - } - } catch { - // ignore - } - } - }, 300) - - checkedLocalStorage.current = true - } - - if (queryGroupRef.current && queryTab) { - let updatedValues: Record = {} - try { - const oldValues = JSON.parse(localStorage.getItem(LOCAL_STORAGE_KEY) ?? '') - if (oldValues && typeof oldValues === 'object') { - updatedValues = oldValues - } - } catch { - // ignore - } - - updatedValues[tabIdsRef.current.sort().join(',')] = queryTab - - try { - localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(updatedValues)) - } catch { - // ignore - } - } - }, [queryTab]) - - const switchTab = (id: string) => { - if (queryGroupRef.current) { - if (!searchParams.getAll('queryGroups').includes(queryGroupRef.current)) { - searchParams.append('queryGroups', queryGroupRef.current) - } - searchParams.set(queryGroupRef.current, id) - } - } - - const onTabClick = (id: string) => { - switchTab(id) - onClick?.(id) - } - - return ( - - {/* Tabs does its own validation */} - {childrenUnvalidated} - - ) - } - -export { withQueryParams } -export type { QueryParamsProps } diff --git a/packages/ui-patterns/src/ComplexTabs/withSticky.tsx b/packages/ui-patterns/src/ComplexTabs/withSticky.tsx deleted file mode 100644 index 7e28feab2d8..00000000000 --- a/packages/ui-patterns/src/ComplexTabs/withSticky.tsx +++ /dev/null @@ -1,63 +0,0 @@ -'use client' - -import { PropsWithChildren, useCallback, type FC } from 'react' -import { type TabsProps } from 'ui/src/components/Tabs' - -import { useSticky } from './withSticky.utils' - -interface StickyProps { - stickyTabList?: { - scrollMarginTop?: string - style?: CSSStyleDeclaration - } -} - -/** - * Wrapper around Tabs to make the tab list sticky while the tab content is in - * view. - */ -const withSticky = - >( - Component: FC> - ) => - ({ stickyTabList, onClick, ...props }: Props & StickyProps) => { - const { inView, observedRef, stickyRef } = useSticky({ - enabled: !!stickyTabList, - style: stickyTabList?.style, - }) - - const onClickInternal = useCallback( - (id: string) => { - if (stickyTabList && inView && stickyRef.current) { - let elem = stickyRef.current as Element | null - while (elem && !elem.matches('[role="tabpanel"][data-state="active"]')) { - elem = elem.nextElementSibling - } - if (!elem) return - - const top = elem.getBoundingClientRect().top - ;(elem as HTMLElement).style.scrollMarginTop = stickyTabList?.scrollMarginTop || '0px' - if (top < 0) { - elem.scrollIntoView({ - behavior: window.matchMedia('(prefers-reduced-motion: no-preference)').matches - ? 'smooth' - : 'instant', - }) - } - } - - onClick?.(id) - }, - [!!stickyTabList, inView, onClick] - ) - - return ( - - ) - } - -export { withSticky } diff --git a/packages/ui/index.tsx b/packages/ui/index.tsx index fccd9efd4ea..26e3060c5b2 100644 --- a/packages/ui/index.tsx +++ b/packages/ui/index.tsx @@ -3,10 +3,6 @@ export * from './src/components/Button' export * from './src/components/Icon/IconBackground' -// DISPLAYS - -export * from './src/components/Tabs' - // NAV export * from './src/components/Menu' diff --git a/packages/ui/src/components/Tabs/Tabs.tsx b/packages/ui/src/components/Tabs/Tabs.tsx deleted file mode 100644 index ccc078f1975..00000000000 --- a/packages/ui/src/components/Tabs/Tabs.tsx +++ /dev/null @@ -1,235 +0,0 @@ -'use client' - -import { cva } from 'class-variance-authority' -import { Tabs as TabsPrimitive } from 'radix-ui' -import { - Children, - isValidElement, - useMemo, - useState, - type KeyboardEvent, - type PropsWithChildren, - type RefObject, -} from 'react' - -import { cn } from '../../lib/utils' - -export interface TabsProps { - type?: 'pills' | 'underlined' | 'cards' | 'rounded-pills' - defaultActiveId?: string - activeId?: string - size?: 'tiny' | 'small' | 'medium' | 'large' | 'xlarge' - block?: boolean - tabBarGutter?: number - tabBarStyle?: React.CSSProperties - onChange?: any - onClick?: any - scrollable?: boolean - wrappable?: boolean - addOnBefore?: React.ReactNode - addOnAfter?: React.ReactNode - listClassNames?: string - baseClassNames?: string - refs?: { - base: RefObject | ((elem: HTMLDivElement | null) => void) - list: RefObject | ((elem: HTMLDivElement | null) => void) - } -} - -interface TabsSubComponents { - Panel: React.FC> -} - -export const tabsListVariants = cva(cn('flex'), { - variants: { - type: { - pills: 'space-x-1', - underlined: 'items-center border-b border-secondary', - cards: '', - 'rounded-pills': 'flex-wrap gap-2', - }, - scrollable: { - true: 'overflow-auto whitespace-nowrap no-scrollbar mask-fadeout-right', - }, - wrappable: { - true: 'flex-wrap', - }, - }, -}) - -export const tabsTriggerListVariants = cva( - cn( - 'relative cursor-pointer flex items-center space-x-2 text-center transition focus:outline-hidden focus-visible:ring-3 focus-visible:ring-foreground-muted focus-visible:border-foreground-muted' - ), - { - variants: { - type: { - pills: 'shadow-xs rounded-sm border', - underlined: 'text-foreground-lighter', - cards: '', - 'rounded-pills': 'shadow-xs rounded-full', - }, - size: { - tiny: 'text-xs px-2.5 py-1', - small: 'text-base md:text-sm leading-4 px-3 py-2', - medium: 'text-base md:text-sm px-4 py-2', - large: 'text-base px-4 py-2', - xlarge: 'text-base px-6 py-3', - }, - block: { - true: 'w-full flex items-center justify-center', - }, - isActive: { - false: 'hover:text-foreground', - }, - }, - compoundVariants: [ - { - type: 'pills', - isActive: true, - className: 'bg-selection text-foreground border-stronger', - }, - { - type: 'pills', - isActive: false, - className: - 'bg-background border-strong hover:border-foreground-muted text-foreground-muted', - }, - { - type: 'underlined', - isActive: true, - className: '!text-foreground border-b-2 border-foreground', - }, - { - type: 'rounded-pills', - isActive: true, - className: 'bg-foreground text-background border-foreground', - }, - { - type: 'rounded-pills', - isActive: false, - className: - 'bg-surface-200 hover:bg-surface-300 hover:border-foreground-lighter text-foreground-lighter', - }, - ], - } -) - -/** - * @deprecated Use `import { Tabs_shadcn_ } from "ui"` instead - */ -const Tabs: React.FC> & TabsSubComponents = ({ - defaultActiveId, - activeId, - type = 'pills', - size = 'tiny', - block, - onChange, - onClick, - scrollable, - wrappable, - addOnBefore, - addOnAfter, - listClassNames, - baseClassNames, - refs, - children: _children, -}) => { - // Children.toArray clones elements (to assign keys) and accesses element.ref - // internally, which triggers a React 19 warning. Children.forEach iterates - // without cloning, so it never touches .ref - // todo: remove this when we upgrade the radix packages to support react 19 - const childrenArr: PanelPropsProps[] = [] - Children.forEach(_children, (child) => { - if (isValidElement(child)) childrenArr.push(child as unknown as PanelPropsProps) - }) - const children = childrenArr - - const [activeTab, setActiveTab] = useState( - activeId ?? - defaultActiveId ?? - // if no defaultActiveId is set use the first panel - children?.[0]?.props?.id - ) - - useMemo(() => { - if (activeId && activeId !== activeTab) setActiveTab(activeId) - }, [activeId]) - - function onTabClick(id: string) { - onClick?.(id) - if (id !== activeTab) { - onChange?.(id) - setActiveTab(id) - } - } - - return ( - - - {addOnBefore} - {children.map((tab) => { - const isActive = activeTab === tab.props.id - - return ( - ) => { - if (e.key === 'Enter') { - e.preventDefault() - onTabClick(tab.props.id) - } - }} - onClick={() => onTabClick(tab.props.id)} - key={`${tab.props.id}-tab-button`} - value={tab.props.id} - className={tabsTriggerListVariants({ type, isActive, size, block })} - > - {tab.props.icon} - {tab.props.label} - {tab.props.iconRight} - - ) - })} - {addOnAfter} - - {children as any} - - ) -} - -// bit of a hack because we map over the JSX in the parent component -interface PanelPropsProps { - props: PanelProps -} - -interface PanelProps { - id: string - label?: string - icon?: React.ReactNode - iconRight?: React.ReactNode - className?: string -} - -/** - * @deprecated Use ./TabsContent_Shadcn_ instead - */ -export const Panel: React.FC> = ({ children, id, className }) => { - return ( - - {children} - - ) -} - -Tabs.Panel = Panel -export default Tabs diff --git a/packages/ui/src/components/Tabs/index.tsx b/packages/ui/src/components/Tabs/index.tsx deleted file mode 100644 index dc2c0ed0446..00000000000 --- a/packages/ui/src/components/Tabs/index.tsx +++ /dev/null @@ -1 +0,0 @@ -export { default as Tabs, type TabsProps } from './Tabs' diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bb806ad544f..8229cc187f9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -384,6 +384,9 @@ importers: beautiful-mermaid: specifier: ^1.1.3 version: 1.1.3 + class-variance-authority: + specifier: ^0.7.1 + version: 0.7.1 common: specifier: workspace:* version: link:../../packages/common