@xsolla/xui-b2c-popover 0.209.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/Popover.tsx","../../../../foundation/primitives-native/src/Box.tsx","../../src/PopoverRoot.native.tsx","../../src/PopoverTitleBar.tsx","../../src/usePopoverDrag.ts","../../src/useDeviceType.ts","../../src/usePopover.ts"],"sourcesContent":["import { forwardRef, useCallback, useEffect, useRef, useState } from \"react\";\n// @ts-expect-error - this will be resolved at build time\nimport { Box } from \"@xsolla/xui-primitives\";\nimport { radiusScale, useResolvedTheme } from \"@xsolla/xui-core\";\nimport type { PopoverProps } from \"./types\";\nimport { PopoverRoot } from \"./PopoverRoot\";\nimport { PopoverTitleBar } from \"./PopoverTitleBar\";\nimport { usePopoverDrag } from \"./usePopoverDrag\";\nimport { useDeviceType } from \"./useDeviceType\";\n\nconst FOCUSABLE_SELECTORS =\n 'button:not([disabled]), [href], input:not([disabled]):not([type=\"hidden\"]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex=\"-1\"])';\n\n/** Figma `idPopover`: 400px fixed panel on desktop and tablet. */\nconst DEFAULT_WIDTH = 400;\n/** Figma `idPopover`: 16px inset, 24px between stacked sections. */\nconst PANEL_PADDING = 16;\nconst SECTION_GAP = 24;\n/** Radius of the `boxShadow&BlurPopover` surface — `radiusScale[\"200\"]`. */\nconst PANEL_RADIUS = radiusScale[\"200\"];\n/** Backdrop blur from the same Figma effect style. */\nconst BACKDROP_BLUR = \"blur(24px)\";\nconst ANIMATION_DURATION = 200;\n\nconst isElementVisible = (el: HTMLElement): boolean => {\n if (el.offsetParent === null && getComputedStyle(el).position !== \"fixed\")\n return false;\n const style = getComputedStyle(el);\n return style.visibility !== \"hidden\" && style.display !== \"none\";\n};\n\nconst getFocusableElements = (container: HTMLElement): HTMLElement[] =>\n Array.from(\n container.querySelectorAll<HTMLElement>(FOCUSABLE_SELECTORS),\n ).filter(isElementVisible);\n\n/**\n * Responsive overlay panel for player-facing surfaces — the container behind\n * the Xsolla ID profile popover.\n *\n * On `desktop` and `tablet` it presents as a 400px floating panel over a\n * scrim, optionally draggable by its title bar. On `mobile` the same content\n * takes over the whole screen as a page. The bucket is measured from the\n * viewport by `useDeviceType`; pass `device` only to override that.\n *\n * Everything inside is a slot — `titleBar`, `header`, `children`, `footer` —\n * which is how the Figma `idPopover` composes its variants (the component set\n * declares one property, `Device`; every other difference between the usage\n * examples is different content in the same slots).\n *\n * ```tsx\n * <Popover\n * open={open}\n * onClose={() => setOpen(false)}\n * title=\"Profile\"\n * header={<Greeting />}\n * footer={<SignOut />}\n * >\n * {sections}\n * </Popover>\n * ```\n */\nexport const Popover = forwardRef<HTMLDivElement, PopoverProps>(\n (\n {\n open = false,\n onClose,\n device: deviceProp,\n width = DEFAULT_WIDTH,\n draggable = false,\n initialPosition,\n onPositionChange,\n title,\n pinned,\n onPinnedChange,\n showCloseButton = true,\n titleBar,\n header,\n children,\n footer,\n closeOnOverlayClick = true,\n closeOnEscape = true,\n scrim = true,\n backgroundColor,\n blur = true,\n styled: styledProps,\n initialFocusRef,\n \"aria-label\": ariaLabel,\n testID,\n themeMode,\n themeProductContext = \"b2c\",\n },\n ref,\n ) => {\n const { theme } = useResolvedTheme({ themeMode, themeProductContext });\n const device = useDeviceType(deviceProp);\n const fullScreen = device === \"mobile\";\n // \"Buttons for floating are used when you can change the position of the\n // popover on a screen. Don't use it […] on mobile.\" — Figma spec.\n const canDrag = draggable && !fullScreen;\n // Pinning exists so a click on the page behind cannot dismiss the panel,\n // and a full-screen page has no outside to click.\n const dismissOnScrim = closeOnOverlayClick && !pinned && !fullScreen;\n\n const [mounted, setMounted] = useState(open);\n const dialogRef = useRef<HTMLDivElement>(null);\n const previousActiveElement = useRef<HTMLElement | null>(null);\n\n const { position, panelRef, dragHandleProps } = usePopoverDrag({\n enabled: canDrag,\n initialPosition,\n onPositionChange,\n });\n\n useEffect(() => {\n if (open) setMounted(true);\n }, [open]);\n\n useEffect(() => {\n if (!open || typeof document === \"undefined\") return;\n\n previousActiveElement.current = document.activeElement as HTMLElement;\n const focusTarget =\n initialFocusRef?.current ||\n (dialogRef.current && getFocusableElements(dialogRef.current)[0]);\n\n if (focusTarget) focusTarget.focus();\n else dialogRef.current?.focus();\n\n // Focus is restored in onExited so it does not jump back to the page\n // while the panel is still on screen.\n }, [open, initialFocusRef]);\n\n useEffect(() => {\n if (!open || !closeOnEscape || !onClose) return;\n if (typeof document === \"undefined\") return;\n const handleKeyDown = (event: KeyboardEvent) => {\n if (event.key === \"Escape\") onClose();\n };\n document.addEventListener(\"keydown\", handleKeyDown);\n return () => document.removeEventListener(\"keydown\", handleKeyDown);\n }, [open, closeOnEscape, onClose]);\n\n const handleKeyDown = useCallback((event: React.KeyboardEvent) => {\n if (event.key !== \"Tab\" || !dialogRef.current) return;\n if (typeof document === \"undefined\") return;\n\n const focusableElements = getFocusableElements(dialogRef.current);\n const firstElement = focusableElements[0];\n const lastElement = focusableElements[focusableElements.length - 1];\n\n if (!firstElement) {\n event.preventDefault();\n return;\n }\n\n if (event.shiftKey && document.activeElement === firstElement) {\n event.preventDefault();\n lastElement.focus();\n } else if (!event.shiftKey && document.activeElement === lastElement) {\n event.preventDefault();\n firstElement.focus();\n }\n }, []);\n\n // One node feeds three consumers: the focus-trap/Escape logic, the drag\n // hook's measurements, and the caller's forwarded ref.\n const setPanelNode = useCallback(\n (node: HTMLDivElement | null) => {\n type MutableRef = React.MutableRefObject<HTMLDivElement | null>;\n (dialogRef as MutableRef).current = node;\n (panelRef as MutableRef).current = node;\n if (typeof ref === \"function\") ref(node);\n else if (ref) (ref as MutableRef).current = node;\n },\n [panelRef, ref],\n );\n\n const handleExited = useCallback(() => {\n setMounted(false);\n previousActiveElement.current?.focus();\n }, []);\n\n if (!mounted) return null;\n\n const closeControl = showCloseButton ? onClose : undefined;\n const hasDefaultTitleBar =\n !titleBar && (title || closeControl || canDrag);\n const accessibleName =\n ariaLabel ?? (typeof title === \"string\" ? title : undefined);\n\n return (\n <PopoverRoot\n open={open}\n onExited={handleExited}\n onBackdropClick={dismissOnScrim ? onClose : undefined}\n scrimColor={\n scrim && !fullScreen\n ? (theme.colors.layer?.scrim ?? \"rgba(0, 0, 0, 0.75)\")\n : undefined\n }\n fullScreen={fullScreen}\n position={position}\n animationDuration={ANIMATION_DURATION}\n >\n <Box\n testID={testID}\n ref={setPanelNode}\n role=\"dialog\"\n aria-modal={fullScreen || scrim ? \"true\" : undefined}\n aria-label={accessibleName}\n data-popover-device={device}\n data-popover-pinned={pinned ? \"true\" : undefined}\n tabIndex={-1}\n onKeyDown={handleKeyDown}\n style={{\n boxSizing: \"border-box\",\n display: \"flex\",\n flexDirection: \"column\",\n alignItems: \"center\",\n gap: SECTION_GAP,\n width: fullScreen ? \"100%\" : width,\n maxWidth: \"100%\",\n height: fullScreen ? \"100%\" : undefined,\n maxHeight: \"100%\",\n outline: \"none\",\n color: theme.colors.content.primary,\n backgroundColor: backgroundColor ?? theme.colors.background.primary,\n paddingLeft: PANEL_PADDING,\n paddingRight: PANEL_PADDING,\n // The Figma mobile frame carries only a bottom inset — its top bars\n // are full-bleed, and its children supply their own side padding.\n // A slot component cannot assume the caller re-pads for one\n // breakpoint, so the sides stay inset and only the top is dropped.\n paddingTop: fullScreen ? 0 : PANEL_PADDING,\n paddingBottom: PANEL_PADDING,\n borderRadius: fullScreen ? 0 : PANEL_RADIUS,\n border: fullScreen\n ? undefined\n : `1px solid ${theme.colors.border.secondary}`,\n boxShadow: fullScreen ? undefined : theme.shadow.popover,\n backdropFilter: !fullScreen && blur ? BACKDROP_BLUR : undefined,\n ...styledProps,\n }}\n >\n {titleBar ??\n (hasDefaultTitleBar ? (\n <PopoverTitleBar\n title={title}\n draggable={canDrag}\n dragHandleProps={dragHandleProps}\n pinned={pinned}\n onPinnedChange={onPinnedChange}\n onClose={closeControl}\n testID={testID ? `${testID}-title-bar` : undefined}\n />\n ) : null)}\n\n {header && (\n <div style={{ flexShrink: 0, width: \"100%\" }}>{header}</div>\n )}\n\n <div\n style={{\n flex: 1,\n minHeight: 0,\n width: \"100%\",\n overflowY: \"auto\",\n overscrollBehavior: \"contain\",\n }}\n >\n {children}\n </div>\n\n {footer && (\n <div style={{ flexShrink: 0, width: \"100%\" }}>{footer}</div>\n )}\n </Box>\n </PopoverRoot>\n );\n },\n);\n\nPopover.displayName = \"Popover\";\n","import React from \"react\";\nimport {\n View,\n Pressable,\n Image,\n ViewStyle,\n ImageStyle,\n DimensionValue,\n AnimatableNumericValue,\n AccessibilityRole,\n} from \"react-native\";\nimport { BoxProps } from \"@xsolla/xui-primitives-core\";\n\nconst roleToAccessibilityRole: Record<string, AccessibilityRole> = {\n button: \"button\",\n link: \"link\",\n img: \"image\",\n image: \"image\",\n summary: \"summary\",\n switch: \"switch\",\n checkbox: \"checkbox\",\n radio: \"radio\",\n heading: \"header\",\n text: \"text\",\n search: \"search\",\n alert: \"alert\",\n none: \"none\",\n presentation: \"none\",\n progressbar: \"progressbar\",\n scrollbar: \"scrollbar\",\n adjustable: \"adjustable\",\n tab: \"tab\",\n menu: \"menu\",\n menuitem: \"menuitem\",\n list: \"list\",\n timer: \"timer\",\n};\n\n/** RN ViewStyle.gap is typed as number (Yoga gap), not DimensionValue. */\nconst toGapValue = (gap: number | string | undefined): number | undefined => {\n if (gap === undefined) return undefined;\n if (typeof gap === \"number\") return gap;\n const pxMatch = /^(\\d+(?:\\.\\d+)?)px$/.exec(gap);\n if (pxMatch) return Number(pxMatch[1]);\n const asNumber = Number(gap);\n return Number.isFinite(asNumber) ? asNumber : undefined;\n};\n\nexport const Box: React.FC<BoxProps> = ({\n children,\n onPress,\n onLayout,\n onMoveShouldSetResponder,\n onResponderGrant,\n onResponderMove,\n onResponderRelease,\n onResponderTerminate,\n backgroundColor,\n borderColor,\n borderWidth,\n borderBottomWidth,\n borderBottomColor,\n borderTopWidth,\n borderTopColor,\n borderLeftWidth,\n borderLeftColor,\n borderRightWidth,\n borderRightColor,\n borderRadius,\n borderStyle,\n height,\n padding,\n paddingHorizontal,\n paddingVertical,\n margin,\n marginTop,\n marginBottom,\n marginLeft,\n marginRight,\n flexDirection,\n flexWrap,\n alignItems,\n justifyContent,\n alignSelf,\n position,\n top,\n bottom,\n left,\n right,\n width,\n minWidth,\n minHeight,\n maxWidth,\n maxHeight,\n flex,\n flexShrink,\n gap,\n overflow,\n opacity,\n zIndex,\n hoverStyle,\n pressStyle,\n style,\n disabled,\n \"data-testid\": dataTestId,\n testID,\n as,\n src,\n alt,\n ...rest\n}) => {\n const getContainerStyle = (pressed?: boolean): ViewStyle => ({\n backgroundColor:\n pressed && pressStyle?.backgroundColor\n ? pressStyle.backgroundColor\n : backgroundColor,\n borderColor,\n borderWidth,\n borderBottomWidth,\n borderBottomColor,\n borderTopWidth,\n borderTopColor,\n borderLeftWidth,\n borderLeftColor,\n borderRightWidth,\n borderRightColor,\n borderRadius: borderRadius as AnimatableNumericValue,\n borderStyle: borderStyle as ViewStyle[\"borderStyle\"],\n overflow,\n opacity: opacity ?? (disabled ? 0.5 : undefined),\n zIndex,\n height: height as DimensionValue,\n width: width as DimensionValue,\n minWidth: minWidth as DimensionValue,\n minHeight: minHeight as DimensionValue,\n maxWidth: maxWidth as DimensionValue,\n maxHeight: maxHeight as DimensionValue,\n padding: padding as DimensionValue,\n paddingHorizontal: paddingHorizontal as DimensionValue,\n paddingVertical: paddingVertical as DimensionValue,\n margin: margin as DimensionValue,\n marginTop: marginTop as DimensionValue,\n marginBottom: marginBottom as DimensionValue,\n marginLeft: marginLeft as DimensionValue,\n marginRight: marginRight as DimensionValue,\n flexDirection,\n flexWrap,\n alignItems,\n justifyContent,\n alignSelf,\n position: position as ViewStyle[\"position\"],\n top: top as DimensionValue,\n bottom: bottom as DimensionValue,\n left: left as DimensionValue,\n right: right as DimensionValue,\n flex,\n flexShrink,\n gap: toGapValue(gap),\n ...(style as ViewStyle),\n });\n\n const finalTestID = dataTestId || testID;\n\n // Map web a11y props; drop the rest of the web-only attributes before\n // spreading onto RN hosts (unknown props warn at runtime).\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n const {\n role,\n tabIndex,\n onKeyDown,\n onKeyUp,\n \"aria-label\": ariaLabel,\n \"aria-labelledby\": _ariaLabelledBy,\n \"aria-current\": _ariaCurrent,\n \"aria-disabled\": ariaDisabled,\n \"aria-live\": ariaLive,\n \"aria-busy\": ariaBusy,\n className,\n \"data-testid\": _dataTestId,\n cursor: _cursor,\n type: _type,\n ...nativeRest\n } = rest as Record<string, unknown>;\n\n const accessibilityRole =\n (typeof role === \"string\" && roleToAccessibilityRole[role]) ||\n (as === \"button\" ? \"button\" : undefined);\n const isDisabled = Boolean(disabled || ariaDisabled);\n const accessibilityState = {\n disabled: isDisabled || undefined,\n busy: Boolean(ariaBusy) || undefined,\n };\n const accessibilityLiveRegion =\n ariaLive === \"polite\" || ariaLive === \"assertive\" ? ariaLive : undefined;\n\n // Handle as=\"img\" for React Native\n if (as === \"img\" && src) {\n const imageStyle: ImageStyle = {\n width: width as DimensionValue,\n height: height as DimensionValue,\n borderRadius: borderRadius as number,\n position: position as ImageStyle[\"position\"],\n top: top as DimensionValue,\n bottom: bottom as DimensionValue,\n left: left as DimensionValue,\n right: right as DimensionValue,\n ...(style as ImageStyle),\n };\n\n return (\n <Image\n source={{ uri: src }}\n style={imageStyle}\n testID={finalTestID}\n accessibilityLabel={typeof ariaLabel === \"string\" ? ariaLabel : alt}\n resizeMode=\"cover\"\n {...nativeRest}\n />\n );\n }\n\n // Prefer Pressable whenever the node is interactive or explicitly a button,\n // so disabled / aria-disabled controls stay in the a11y tree as buttons.\n if (onPress || as === \"button\") {\n return (\n <Pressable\n onPress={!isDisabled && onPress ? onPress : undefined}\n disabled={isDisabled}\n onLayout={onLayout}\n onMoveShouldSetResponder={onMoveShouldSetResponder}\n onResponderGrant={onResponderGrant}\n onResponderMove={onResponderMove}\n onResponderRelease={onResponderRelease}\n onResponderTerminate={onResponderTerminate}\n style={({ pressed }) => getContainerStyle(pressed)}\n testID={finalTestID}\n accessibilityRole={accessibilityRole}\n accessibilityLabel={\n typeof ariaLabel === \"string\" ? ariaLabel : undefined\n }\n accessibilityState={accessibilityState}\n accessibilityLiveRegion={accessibilityLiveRegion}\n {...nativeRest}\n >\n {children}\n </Pressable>\n );\n }\n\n return (\n <View\n style={getContainerStyle()}\n testID={finalTestID}\n onLayout={onLayout}\n onMoveShouldSetResponder={onMoveShouldSetResponder}\n onResponderGrant={onResponderGrant}\n onResponderMove={onResponderMove}\n onResponderRelease={onResponderRelease}\n onResponderTerminate={onResponderTerminate}\n accessibilityRole={accessibilityRole}\n accessibilityLabel={typeof ariaLabel === \"string\" ? ariaLabel : undefined}\n accessibilityState={accessibilityState}\n accessibilityLiveRegion={accessibilityLiveRegion}\n {...nativeRest}\n >\n {children}\n </View>\n );\n};\n","import { memo } from \"react\";\nimport type { PopoverRootProps } from \"./types\";\n\n// Web portal not available on React Native.\n// A native popover would present through RN's own Modal / Animated APIs.\nexport const PopoverRoot = memo((_props: PopoverRootProps) => {\n return null;\n});\n\nPopoverRoot.displayName = \"PopoverRoot\";\n","import { memo } from \"react\";\nimport { IconButton } from \"@xsolla/xui-button\";\nimport { Drag, Pin, Remove, Unpin } from \"@xsolla/xui-icons-base\";\nimport { Typography } from \"@xsolla/xui-typography\";\nimport type { PopoverTitleBarProps } from \"./types\";\n\n/**\n * The floating title row from the Figma `idPopover` spec: grab handle, title,\n * pin toggle, close button.\n *\n * `dragHandleProps` is attached to the whole row rather than just the grip, so\n * the title text is draggable too. Presses that originate inside a `<button>`\n * (pin, close) are filtered out by `usePopoverDrag`, and the grip stays\n * focusable so the row can also be moved with the arrow keys.\n */\nexport const PopoverTitleBar = memo(\n ({\n title,\n draggable,\n dragHandleProps,\n pinned,\n onPinnedChange,\n onClose,\n testID,\n }: PopoverTitleBarProps) => (\n <div\n data-testid={testID}\n style={{\n flexShrink: 0,\n width: \"100%\",\n display: \"flex\",\n flexDirection: \"row\",\n alignItems: \"center\",\n gap: 8,\n cursor: draggable ? \"grab\" : undefined,\n }}\n {...(draggable ? dragHandleProps : {})}\n >\n {draggable && (\n <span\n data-xui-popover-drag-handle=\"\"\n role=\"button\"\n tabIndex={0}\n aria-label=\"Move popover\"\n style={{ display: \"inline-flex\", flexShrink: 0 }}\n >\n <Drag />\n </span>\n )}\n\n <div style={{ flex: 1, minWidth: 0, overflow: \"hidden\" }}>\n {title && (\n <Typography variant=\"bodyLgAccent\" noWrap>\n {title}\n </Typography>\n )}\n </div>\n\n {onPinnedChange && (\n <IconButton\n variant=\"secondary\"\n tone=\"mono\"\n size=\"xs\"\n icon={pinned ? <Unpin /> : <Pin />}\n onPress={() => onPinnedChange(!pinned)}\n aria-label={pinned ? \"Unpin popover\" : \"Pin popover\"}\n aria-pressed={pinned}\n />\n )}\n\n {onClose && (\n <IconButton\n variant=\"secondary\"\n tone=\"mono\"\n size=\"xs\"\n icon={<Remove />}\n onPress={onClose}\n aria-label=\"Close popover\"\n />\n )}\n </div>\n ),\n);\n\nPopoverTitleBar.displayName = \"PopoverTitleBar\";\n","import {\n useCallback,\n useEffect,\n useRef,\n useState,\n type KeyboardEvent as ReactKeyboardEvent,\n type PointerEvent as ReactPointerEvent,\n type RefObject,\n} from \"react\";\nimport type { PopoverDragHandleProps, PopoverPosition } from \"./types\";\n\nexport interface UsePopoverDragOptions {\n /** Drag is a no-op while this is false (mobile, or `draggable={false}`). */\n enabled: boolean;\n /** Starting viewport offset. `undefined` leaves the panel centred. */\n initialPosition?: PopoverPosition;\n onPositionChange?: (position: PopoverPosition) => void;\n}\n\nexport interface UsePopoverDragReturn {\n /** `undefined` until the panel has been positioned, so it stays centred. */\n position?: PopoverPosition;\n /** Attach to the panel so the hook can measure it for clamping. */\n panelRef: RefObject<HTMLDivElement>;\n /** Spread onto the element that starts a drag. */\n dragHandleProps: PopoverDragHandleProps;\n isDragging: boolean;\n}\n\n/** Distance an arrow key moves the panel. */\nconst KEYBOARD_STEP = 16;\n/** Keeps a dragged panel from being pushed fully off-screen. */\nconst EDGE_MARGIN = 8;\n\n/** True when the event originated on a real control rather than the row. */\nconst isInteractive = (target: EventTarget | null): boolean =>\n target instanceof Element && target.closest(\"button, a, input\") !== null;\n\nconst clampToViewport = (\n position: PopoverPosition,\n panel: HTMLElement | null,\n): PopoverPosition => {\n if (!panel || typeof window === \"undefined\") return position;\n const { width, height } = panel.getBoundingClientRect();\n const maxX = Math.max(EDGE_MARGIN, window.innerWidth - width - EDGE_MARGIN);\n const maxY = Math.max(EDGE_MARGIN, window.innerHeight - height - EDGE_MARGIN);\n return {\n x: Math.min(Math.max(position.x, EDGE_MARGIN), maxX),\n y: Math.min(Math.max(position.y, EDGE_MARGIN), maxY),\n };\n};\n\n/**\n * Pointer- and keyboard-driven repositioning for a floating popover panel.\n *\n * The panel stays in its centred flow position until the first drag, at which\n * point the hook switches it to an absolute viewport offset. Positions are\n * always clamped so at least `EDGE_MARGIN` of the panel stays on screen.\n */\nexport const usePopoverDrag = ({\n enabled,\n initialPosition,\n onPositionChange,\n}: UsePopoverDragOptions): UsePopoverDragReturn => {\n const panelRef = useRef<HTMLDivElement>(null);\n const [position, setPosition] = useState<PopoverPosition | undefined>(\n initialPosition,\n );\n const [isDragging, setIsDragging] = useState(false);\n // Offset between the pointer and the panel's top-left corner, so the panel\n // does not jump to centre itself under the cursor on the first move.\n const grabOffset = useRef<PopoverPosition>({ x: 0, y: 0 });\n\n // Dropping out of draggable mode (e.g. a resize into the mobile variant)\n // must release the absolute offset, otherwise the full-screen page inherits\n // a stale left/top and renders off-centre.\n useEffect(() => {\n if (!enabled) setPosition(undefined);\n }, [enabled]);\n\n const commit = useCallback(\n (next: PopoverPosition) => {\n const clamped = clampToViewport(next, panelRef.current);\n setPosition(clamped);\n onPositionChange?.(clamped);\n },\n [onPositionChange],\n );\n\n const onPointerDown = useCallback(\n (event: ReactPointerEvent) => {\n if (!enabled || event.button !== 0) return;\n // The handlers sit on the whole title row, so a press on the pin or\n // close button would otherwise start a drag instead of clicking.\n if (isInteractive(event.target)) return;\n const panel = panelRef.current;\n if (!panel) return;\n\n event.preventDefault();\n const rect = panel.getBoundingClientRect();\n grabOffset.current = {\n x: event.clientX - rect.left,\n y: event.clientY - rect.top,\n };\n setIsDragging(true);\n },\n [enabled],\n );\n\n const onKeyDown = useCallback(\n (event: ReactKeyboardEvent) => {\n if (!enabled) return;\n // Only the grip nudges the panel — arrow keys elsewhere in the row\n // belong to whatever control has focus.\n if (\n !(event.target instanceof Element) ||\n !event.target.closest(\"[data-xui-popover-drag-handle]\")\n ) {\n return;\n }\n const deltas: Record<string, PopoverPosition> = {\n ArrowUp: { x: 0, y: -KEYBOARD_STEP },\n ArrowDown: { x: 0, y: KEYBOARD_STEP },\n ArrowLeft: { x: -KEYBOARD_STEP, y: 0 },\n ArrowRight: { x: KEYBOARD_STEP, y: 0 },\n };\n const delta = deltas[event.key];\n if (!delta) return;\n\n event.preventDefault();\n // The first arrow key press has no offset yet, so anchor on wherever the\n // centred panel currently sits.\n const rect = panelRef.current?.getBoundingClientRect();\n const base = position ?? { x: rect?.left ?? 0, y: rect?.top ?? 0 };\n commit({ x: base.x + delta.x, y: base.y + delta.y });\n },\n [commit, enabled, position],\n );\n\n useEffect(() => {\n if (!isDragging || typeof document === \"undefined\") return;\n\n const handleMove = (event: PointerEvent) => {\n commit({\n x: event.clientX - grabOffset.current.x,\n y: event.clientY - grabOffset.current.y,\n });\n };\n const stop = () => setIsDragging(false);\n\n document.addEventListener(\"pointermove\", handleMove);\n document.addEventListener(\"pointerup\", stop);\n document.addEventListener(\"pointercancel\", stop);\n return () => {\n document.removeEventListener(\"pointermove\", handleMove);\n document.removeEventListener(\"pointerup\", stop);\n document.removeEventListener(\"pointercancel\", stop);\n };\n }, [commit, isDragging]);\n\n return {\n position: enabled ? position : undefined,\n panelRef,\n dragHandleProps: { onPointerDown, onKeyDown },\n isDragging,\n };\n};\n","import { useEffect, useState } from \"react\";\nimport type { PopoverDevice } from \"./types\";\n\n/**\n * Viewport thresholds from the Figma \"Breakpoints\" frame\n * (file `SOOtaT80JAVJf7PKlFnt7Q`, node `6393:1137`).\n */\nexport const POPOVER_BREAKPOINTS = {\n /** Widths at or below this are `mobile` (Figma: \"Mobile: 320-768px\"). */\n mobileMax: 768,\n /** Widths at or below this, but above `mobileMax`, are `tablet` (Figma: \"Tablet: 769-1023px\"). */\n tabletMax: 1023,\n} as const;\n\n/** Maps a viewport width in CSS pixels onto a {@link PopoverDevice} bucket. */\nexport const resolveDevice = (width: number): PopoverDevice => {\n if (width <= POPOVER_BREAKPOINTS.mobileMax) return \"mobile\";\n if (width <= POPOVER_BREAKPOINTS.tabletMax) return \"tablet\";\n return \"desktop\";\n};\n\n/**\n * Resolves the active device bucket from the viewport width, re-resolving on\n * resize. Pass `override` to short-circuit measurement entirely — the override\n * is returned verbatim and no listener is attached.\n *\n * Server-side (no `window`) the hook reports `desktop`, which matches the\n * Figma default variant, and corrects itself on the first client effect.\n */\nexport const useDeviceType = (override?: PopoverDevice): PopoverDevice => {\n const [device, setDevice] = useState<PopoverDevice>(() =>\n typeof window === \"undefined\" ? \"desktop\" : resolveDevice(window.innerWidth),\n );\n\n useEffect(() => {\n if (override || typeof window === \"undefined\") return;\n\n const update = () => setDevice(resolveDevice(window.innerWidth));\n // Re-read on mount: the initial state was computed before hydration on SSR.\n update();\n\n window.addEventListener(\"resize\", update);\n return () => window.removeEventListener(\"resize\", update);\n }, [override]);\n\n return override ?? device;\n};\n","import { useCallback, useState } from \"react\";\n\nexport interface UsePopoverOptions {\n onOpen?: () => void;\n onClose?: () => void;\n}\n\nexport interface UsePopoverReturn {\n isOpen: boolean;\n open: () => void;\n close: () => void;\n toggle: () => void;\n}\n\n/** Open/close state for a `Popover`, mirroring `useDrawer`. */\nexport function usePopover(options?: UsePopoverOptions): UsePopoverReturn {\n const [isOpen, setIsOpen] = useState(false);\n\n const open = useCallback(() => {\n setIsOpen(true);\n options?.onOpen?.();\n }, [options]);\n\n const close = useCallback(() => {\n setIsOpen(false);\n options?.onClose?.();\n }, [options]);\n\n const toggle = useCallback(() => {\n setIsOpen((previous) => {\n if (previous) options?.onClose?.();\n else options?.onOpen?.();\n return !previous;\n });\n }, [options]);\n\n return { isOpen, open, close, toggle };\n}\n"],"mappings":"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","names":["useCallback","useEffect","useRef","useState","memo","jsx","useEffect","useState","jsx","jsxs","useState","useRef","useEffect","handleKeyDown","useCallback","useCallback","useState"]}
package/package.json ADDED
@@ -0,0 +1,60 @@
1
+ {
2
+ "name": "@xsolla/xui-b2c-popover",
3
+ "version": "0.209.0",
4
+ "main": "./web/index.js",
5
+ "module": "./web/index.mjs",
6
+ "types": "./web/index.d.ts",
7
+ "scripts": {
8
+ "build": "yarn build:web && yarn build:native",
9
+ "build:web": "PLATFORM=web tsup",
10
+ "build:native": "PLATFORM=native tsup",
11
+ "test": "vitest",
12
+ "test:run": "vitest run",
13
+ "test:coverage": "vitest run --coverage"
14
+ },
15
+ "dependencies": {
16
+ "@xsolla/xui-button": "0.209.0",
17
+ "@xsolla/xui-core": "0.209.0",
18
+ "@xsolla/xui-icons-base": "0.209.0",
19
+ "@xsolla/xui-primitives-core": "0.209.0",
20
+ "@xsolla/xui-typography": "0.209.0"
21
+ },
22
+ "peerDependencies": {
23
+ "react": ">=16.8.0",
24
+ "react-dom": ">=16"
25
+ },
26
+ "devDependencies": {
27
+ "@testing-library/jest-dom": "^6.9.1",
28
+ "@testing-library/react": "^14.1.2",
29
+ "@vitest/coverage-v8": "^4.0.18",
30
+ "jsdom": "^24.0.0",
31
+ "react": "^18.0.0",
32
+ "react-dom": "^18.0.0",
33
+ "tsup": "^8.0.0",
34
+ "vitest": "^4.0.18"
35
+ },
36
+ "license": "MIT",
37
+ "sideEffects": false,
38
+ "react-native": "./native/index.js",
39
+ "exports": {
40
+ ".": {
41
+ "react-native": {
42
+ "types": "./native/index.d.ts",
43
+ "import": "./native/index.mjs",
44
+ "require": "./native/index.js"
45
+ },
46
+ "import": {
47
+ "types": "./web/index.d.ts",
48
+ "default": "./web/index.mjs"
49
+ },
50
+ "require": {
51
+ "types": "./web/index.d.ts",
52
+ "default": "./web/index.js"
53
+ },
54
+ "default": {
55
+ "types": "./web/index.d.ts",
56
+ "default": "./web/index.js"
57
+ }
58
+ }
59
+ }
60
+ }
@@ -0,0 +1,255 @@
1
+ import * as react from 'react';
2
+ import { ReactNode, CSSProperties, RefObject, PointerEvent, KeyboardEvent } from 'react';
3
+ import { ThemeOverrideProps } from '@xsolla/xui-core';
4
+ import * as react_jsx_runtime from 'react/jsx-runtime';
5
+
6
+ /**
7
+ * Device buckets from the Figma "Breakpoints" frame
8
+ * (file `SOOtaT80JAVJf7PKlFnt7Q`, node `6393:1137`):
9
+ *
10
+ * | Device | Viewport | Presentation |
11
+ * | ------- | ------------- | ------------------------------- |
12
+ * | Desktop | 1024px and up | 400px floating panel |
13
+ * | Tablet | 769–1023px | 400px floating panel |
14
+ * | Mobile | 320–768px | full-screen, reads as a page |
15
+ *
16
+ * Mirrors the `Device` variant on the Figma `idPopover` component set
17
+ * (node `7394:4832`), whose options are Desktop (default), Tablet, Mobile.
18
+ */
19
+ type PopoverDevice = "desktop" | "tablet" | "mobile";
20
+ /** Viewport-relative offset of the floating panel's top-left corner. */
21
+ interface PopoverPosition {
22
+ x: number;
23
+ y: number;
24
+ }
25
+ /** Handlers that turn an element into a drag grip for the floating panel. */
26
+ interface PopoverDragHandleProps {
27
+ onPointerDown: (event: PointerEvent) => void;
28
+ onKeyDown: (event: KeyboardEvent) => void;
29
+ }
30
+ interface PopoverProps extends ThemeOverrideProps {
31
+ /** Whether the popover is open. */
32
+ open?: boolean;
33
+ /** Called when the popover should close (Escape, scrim press, close button). */
34
+ onClose?: () => void;
35
+ /**
36
+ * Forces a device bucket instead of measuring the viewport. Useful for
37
+ * Storybook, tests, and product shells that already know their layout.
38
+ * Omit it and `useDeviceType` resolves the bucket from `window.innerWidth`.
39
+ */
40
+ device?: PopoverDevice;
41
+ /**
42
+ * Panel width on `desktop` / `tablet`. Ignored on `mobile`, which fills the
43
+ * viewport.
44
+ * @default 400
45
+ */
46
+ width?: number | string;
47
+ /**
48
+ * Renders the grab handle in the title bar and lets the user reposition the
49
+ * panel. Ignored on `mobile` — per the Figma spec, "Buttons for floating […]
50
+ * Don't use it in other cases and on mobile."
51
+ * @default false
52
+ */
53
+ draggable?: boolean;
54
+ /**
55
+ * Starting viewport offset for a `draggable` panel. Omit to let the panel
56
+ * centre itself until the first drag.
57
+ */
58
+ initialPosition?: PopoverPosition;
59
+ /** Called on every drag frame with the panel's new viewport offset. */
60
+ onPositionChange?: (position: PopoverPosition) => void;
61
+ /** Title shown in the default title bar, to the right of the grab handle. */
62
+ title?: ReactNode;
63
+ /**
64
+ * Whether the popover is pinned. Pinning shows the pin control in its active
65
+ * state and suppresses scrim dismissal. Controlled — the control only renders
66
+ * when `onPinnedChange` is supplied.
67
+ */
68
+ pinned?: boolean;
69
+ /** Called with the next pinned state when the pin control is pressed. */
70
+ onPinnedChange?: (pinned: boolean) => void;
71
+ /**
72
+ * Renders the close (X) control in the default title bar. Set it to `false`
73
+ * for the Figma "No buttons" variant, which has no chrome at all — `onClose`
74
+ * still runs on Escape and on a scrim press, so the popover stays
75
+ * dismissible.
76
+ * @default true
77
+ */
78
+ showCloseButton?: boolean;
79
+ /**
80
+ * Replaces the whole default title bar (grab handle, title, pin, close).
81
+ * You own the grab handle in this case — put `data-xui-popover-drag-handle`
82
+ * on the element that should accept arrow-key nudges.
83
+ */
84
+ titleBar?: ReactNode;
85
+ /**
86
+ * Content pinned above the scrollable body — the logo row, hero image, and
87
+ * greeting in the Xsolla ID profile popover.
88
+ */
89
+ header?: ReactNode;
90
+ /** Scrollable body content. */
91
+ children: ReactNode;
92
+ /** Content pinned below the scrollable body — sign-out button, legal links. */
93
+ footer?: ReactNode;
94
+ /**
95
+ * Whether pressing the scrim closes the popover. Always disabled while
96
+ * `pinned`, and on `mobile`, where a full-screen page has no outside.
97
+ * @default true
98
+ */
99
+ closeOnOverlayClick?: boolean;
100
+ /** Whether pressing Escape closes the popover. @default true */
101
+ closeOnEscape?: boolean;
102
+ /**
103
+ * Renders the dimming scrim behind the panel. Turn it off for a floating
104
+ * panel that should leave the page underneath usable — that is the
105
+ * "Float buttons for desktop" case in the Figma spec.
106
+ * @default true
107
+ */
108
+ scrim?: boolean;
109
+ /**
110
+ * Surface background. Accepts any theme background token or a CSS colour.
111
+ * @default theme.colors.background.primary
112
+ */
113
+ backgroundColor?: string;
114
+ /**
115
+ * Applies the 24px backdrop blur from the Figma `boxShadow&BlurPopover`
116
+ * effect style. Ignored on `mobile`.
117
+ * @default true
118
+ */
119
+ blur?: boolean;
120
+ /** Extra styles merged onto the panel. */
121
+ styled?: CSSProperties;
122
+ /** Element focused when the popover opens. Defaults to the first focusable. */
123
+ initialFocusRef?: RefObject<HTMLElement>;
124
+ /** Accessible name for the dialog. Falls back to a string `title`. */
125
+ "aria-label"?: string;
126
+ testID?: string;
127
+ }
128
+ interface PopoverRootProps {
129
+ children: ReactNode;
130
+ /** Whether the popover is logically open — `false` runs the exit animation. */
131
+ open: boolean;
132
+ /** Called once the exit animation has finished so the parent can unmount. */
133
+ onExited: () => void;
134
+ /** Press handler for the scrim. Omit to make the scrim inert. */
135
+ onBackdropClick?: () => void;
136
+ /** Scrim colour, or `undefined` to render no scrim at all. */
137
+ scrimColor?: string;
138
+ /** Full-bleed layout for the `mobile` variant. */
139
+ fullScreen: boolean;
140
+ /** Absolute viewport offset — set once a draggable panel has been moved. */
141
+ position?: PopoverPosition;
142
+ animationDuration: number;
143
+ testID?: string;
144
+ }
145
+ interface PopoverTitleBarProps {
146
+ title?: ReactNode;
147
+ /** Renders the grab handle and wires it to `dragHandleProps`. */
148
+ draggable: boolean;
149
+ dragHandleProps?: PopoverDragHandleProps;
150
+ pinned?: boolean;
151
+ onPinnedChange?: (pinned: boolean) => void;
152
+ onClose?: () => void;
153
+ testID?: string;
154
+ }
155
+
156
+ /**
157
+ * Responsive overlay panel for player-facing surfaces — the container behind
158
+ * the Xsolla ID profile popover.
159
+ *
160
+ * On `desktop` and `tablet` it presents as a 400px floating panel over a
161
+ * scrim, optionally draggable by its title bar. On `mobile` the same content
162
+ * takes over the whole screen as a page. The bucket is measured from the
163
+ * viewport by `useDeviceType`; pass `device` only to override that.
164
+ *
165
+ * Everything inside is a slot — `titleBar`, `header`, `children`, `footer` —
166
+ * which is how the Figma `idPopover` composes its variants (the component set
167
+ * declares one property, `Device`; every other difference between the usage
168
+ * examples is different content in the same slots).
169
+ *
170
+ * ```tsx
171
+ * <Popover
172
+ * open={open}
173
+ * onClose={() => setOpen(false)}
174
+ * title="Profile"
175
+ * header={<Greeting />}
176
+ * footer={<SignOut />}
177
+ * >
178
+ * {sections}
179
+ * </Popover>
180
+ * ```
181
+ */
182
+ declare const Popover: react.ForwardRefExoticComponent<PopoverProps & react.RefAttributes<HTMLDivElement>>;
183
+
184
+ /**
185
+ * The floating title row from the Figma `idPopover` spec: grab handle, title,
186
+ * pin toggle, close button.
187
+ *
188
+ * `dragHandleProps` is attached to the whole row rather than just the grip, so
189
+ * the title text is draggable too. Presses that originate inside a `<button>`
190
+ * (pin, close) are filtered out by `usePopoverDrag`, and the grip stays
191
+ * focusable so the row can also be moved with the arrow keys.
192
+ */
193
+ declare const PopoverTitleBar: react.MemoExoticComponent<({ title, draggable, dragHandleProps, pinned, onPinnedChange, onClose, testID, }: PopoverTitleBarProps) => react_jsx_runtime.JSX.Element>;
194
+
195
+ interface UsePopoverOptions {
196
+ onOpen?: () => void;
197
+ onClose?: () => void;
198
+ }
199
+ interface UsePopoverReturn {
200
+ isOpen: boolean;
201
+ open: () => void;
202
+ close: () => void;
203
+ toggle: () => void;
204
+ }
205
+ /** Open/close state for a `Popover`, mirroring `useDrawer`. */
206
+ declare function usePopover(options?: UsePopoverOptions): UsePopoverReturn;
207
+
208
+ interface UsePopoverDragOptions {
209
+ /** Drag is a no-op while this is false (mobile, or `draggable={false}`). */
210
+ enabled: boolean;
211
+ /** Starting viewport offset. `undefined` leaves the panel centred. */
212
+ initialPosition?: PopoverPosition;
213
+ onPositionChange?: (position: PopoverPosition) => void;
214
+ }
215
+ interface UsePopoverDragReturn {
216
+ /** `undefined` until the panel has been positioned, so it stays centred. */
217
+ position?: PopoverPosition;
218
+ /** Attach to the panel so the hook can measure it for clamping. */
219
+ panelRef: RefObject<HTMLDivElement>;
220
+ /** Spread onto the element that starts a drag. */
221
+ dragHandleProps: PopoverDragHandleProps;
222
+ isDragging: boolean;
223
+ }
224
+ /**
225
+ * Pointer- and keyboard-driven repositioning for a floating popover panel.
226
+ *
227
+ * The panel stays in its centred flow position until the first drag, at which
228
+ * point the hook switches it to an absolute viewport offset. Positions are
229
+ * always clamped so at least `EDGE_MARGIN` of the panel stays on screen.
230
+ */
231
+ declare const usePopoverDrag: ({ enabled, initialPosition, onPositionChange, }: UsePopoverDragOptions) => UsePopoverDragReturn;
232
+
233
+ /**
234
+ * Viewport thresholds from the Figma "Breakpoints" frame
235
+ * (file `SOOtaT80JAVJf7PKlFnt7Q`, node `6393:1137`).
236
+ */
237
+ declare const POPOVER_BREAKPOINTS: {
238
+ /** Widths at or below this are `mobile` (Figma: "Mobile: 320-768px"). */
239
+ readonly mobileMax: 768;
240
+ /** Widths at or below this, but above `mobileMax`, are `tablet` (Figma: "Tablet: 769-1023px"). */
241
+ readonly tabletMax: 1023;
242
+ };
243
+ /** Maps a viewport width in CSS pixels onto a {@link PopoverDevice} bucket. */
244
+ declare const resolveDevice: (width: number) => PopoverDevice;
245
+ /**
246
+ * Resolves the active device bucket from the viewport width, re-resolving on
247
+ * resize. Pass `override` to short-circuit measurement entirely — the override
248
+ * is returned verbatim and no listener is attached.
249
+ *
250
+ * Server-side (no `window`) the hook reports `desktop`, which matches the
251
+ * Figma default variant, and corrects itself on the first client effect.
252
+ */
253
+ declare const useDeviceType: (override?: PopoverDevice) => PopoverDevice;
254
+
255
+ export { POPOVER_BREAKPOINTS, Popover, type PopoverDevice, type PopoverDragHandleProps, type PopoverPosition, type PopoverProps, type PopoverRootProps, PopoverTitleBar, type PopoverTitleBarProps, type UsePopoverDragOptions, type UsePopoverDragReturn, type UsePopoverOptions, type UsePopoverReturn, resolveDevice, useDeviceType, usePopover, usePopoverDrag };
package/web/index.d.ts ADDED
@@ -0,0 +1,255 @@
1
+ import * as react from 'react';
2
+ import { ReactNode, CSSProperties, RefObject, PointerEvent, KeyboardEvent } from 'react';
3
+ import { ThemeOverrideProps } from '@xsolla/xui-core';
4
+ import * as react_jsx_runtime from 'react/jsx-runtime';
5
+
6
+ /**
7
+ * Device buckets from the Figma "Breakpoints" frame
8
+ * (file `SOOtaT80JAVJf7PKlFnt7Q`, node `6393:1137`):
9
+ *
10
+ * | Device | Viewport | Presentation |
11
+ * | ------- | ------------- | ------------------------------- |
12
+ * | Desktop | 1024px and up | 400px floating panel |
13
+ * | Tablet | 769–1023px | 400px floating panel |
14
+ * | Mobile | 320–768px | full-screen, reads as a page |
15
+ *
16
+ * Mirrors the `Device` variant on the Figma `idPopover` component set
17
+ * (node `7394:4832`), whose options are Desktop (default), Tablet, Mobile.
18
+ */
19
+ type PopoverDevice = "desktop" | "tablet" | "mobile";
20
+ /** Viewport-relative offset of the floating panel's top-left corner. */
21
+ interface PopoverPosition {
22
+ x: number;
23
+ y: number;
24
+ }
25
+ /** Handlers that turn an element into a drag grip for the floating panel. */
26
+ interface PopoverDragHandleProps {
27
+ onPointerDown: (event: PointerEvent) => void;
28
+ onKeyDown: (event: KeyboardEvent) => void;
29
+ }
30
+ interface PopoverProps extends ThemeOverrideProps {
31
+ /** Whether the popover is open. */
32
+ open?: boolean;
33
+ /** Called when the popover should close (Escape, scrim press, close button). */
34
+ onClose?: () => void;
35
+ /**
36
+ * Forces a device bucket instead of measuring the viewport. Useful for
37
+ * Storybook, tests, and product shells that already know their layout.
38
+ * Omit it and `useDeviceType` resolves the bucket from `window.innerWidth`.
39
+ */
40
+ device?: PopoverDevice;
41
+ /**
42
+ * Panel width on `desktop` / `tablet`. Ignored on `mobile`, which fills the
43
+ * viewport.
44
+ * @default 400
45
+ */
46
+ width?: number | string;
47
+ /**
48
+ * Renders the grab handle in the title bar and lets the user reposition the
49
+ * panel. Ignored on `mobile` — per the Figma spec, "Buttons for floating […]
50
+ * Don't use it in other cases and on mobile."
51
+ * @default false
52
+ */
53
+ draggable?: boolean;
54
+ /**
55
+ * Starting viewport offset for a `draggable` panel. Omit to let the panel
56
+ * centre itself until the first drag.
57
+ */
58
+ initialPosition?: PopoverPosition;
59
+ /** Called on every drag frame with the panel's new viewport offset. */
60
+ onPositionChange?: (position: PopoverPosition) => void;
61
+ /** Title shown in the default title bar, to the right of the grab handle. */
62
+ title?: ReactNode;
63
+ /**
64
+ * Whether the popover is pinned. Pinning shows the pin control in its active
65
+ * state and suppresses scrim dismissal. Controlled — the control only renders
66
+ * when `onPinnedChange` is supplied.
67
+ */
68
+ pinned?: boolean;
69
+ /** Called with the next pinned state when the pin control is pressed. */
70
+ onPinnedChange?: (pinned: boolean) => void;
71
+ /**
72
+ * Renders the close (X) control in the default title bar. Set it to `false`
73
+ * for the Figma "No buttons" variant, which has no chrome at all — `onClose`
74
+ * still runs on Escape and on a scrim press, so the popover stays
75
+ * dismissible.
76
+ * @default true
77
+ */
78
+ showCloseButton?: boolean;
79
+ /**
80
+ * Replaces the whole default title bar (grab handle, title, pin, close).
81
+ * You own the grab handle in this case — put `data-xui-popover-drag-handle`
82
+ * on the element that should accept arrow-key nudges.
83
+ */
84
+ titleBar?: ReactNode;
85
+ /**
86
+ * Content pinned above the scrollable body — the logo row, hero image, and
87
+ * greeting in the Xsolla ID profile popover.
88
+ */
89
+ header?: ReactNode;
90
+ /** Scrollable body content. */
91
+ children: ReactNode;
92
+ /** Content pinned below the scrollable body — sign-out button, legal links. */
93
+ footer?: ReactNode;
94
+ /**
95
+ * Whether pressing the scrim closes the popover. Always disabled while
96
+ * `pinned`, and on `mobile`, where a full-screen page has no outside.
97
+ * @default true
98
+ */
99
+ closeOnOverlayClick?: boolean;
100
+ /** Whether pressing Escape closes the popover. @default true */
101
+ closeOnEscape?: boolean;
102
+ /**
103
+ * Renders the dimming scrim behind the panel. Turn it off for a floating
104
+ * panel that should leave the page underneath usable — that is the
105
+ * "Float buttons for desktop" case in the Figma spec.
106
+ * @default true
107
+ */
108
+ scrim?: boolean;
109
+ /**
110
+ * Surface background. Accepts any theme background token or a CSS colour.
111
+ * @default theme.colors.background.primary
112
+ */
113
+ backgroundColor?: string;
114
+ /**
115
+ * Applies the 24px backdrop blur from the Figma `boxShadow&BlurPopover`
116
+ * effect style. Ignored on `mobile`.
117
+ * @default true
118
+ */
119
+ blur?: boolean;
120
+ /** Extra styles merged onto the panel. */
121
+ styled?: CSSProperties;
122
+ /** Element focused when the popover opens. Defaults to the first focusable. */
123
+ initialFocusRef?: RefObject<HTMLElement>;
124
+ /** Accessible name for the dialog. Falls back to a string `title`. */
125
+ "aria-label"?: string;
126
+ testID?: string;
127
+ }
128
+ interface PopoverRootProps {
129
+ children: ReactNode;
130
+ /** Whether the popover is logically open — `false` runs the exit animation. */
131
+ open: boolean;
132
+ /** Called once the exit animation has finished so the parent can unmount. */
133
+ onExited: () => void;
134
+ /** Press handler for the scrim. Omit to make the scrim inert. */
135
+ onBackdropClick?: () => void;
136
+ /** Scrim colour, or `undefined` to render no scrim at all. */
137
+ scrimColor?: string;
138
+ /** Full-bleed layout for the `mobile` variant. */
139
+ fullScreen: boolean;
140
+ /** Absolute viewport offset — set once a draggable panel has been moved. */
141
+ position?: PopoverPosition;
142
+ animationDuration: number;
143
+ testID?: string;
144
+ }
145
+ interface PopoverTitleBarProps {
146
+ title?: ReactNode;
147
+ /** Renders the grab handle and wires it to `dragHandleProps`. */
148
+ draggable: boolean;
149
+ dragHandleProps?: PopoverDragHandleProps;
150
+ pinned?: boolean;
151
+ onPinnedChange?: (pinned: boolean) => void;
152
+ onClose?: () => void;
153
+ testID?: string;
154
+ }
155
+
156
+ /**
157
+ * Responsive overlay panel for player-facing surfaces — the container behind
158
+ * the Xsolla ID profile popover.
159
+ *
160
+ * On `desktop` and `tablet` it presents as a 400px floating panel over a
161
+ * scrim, optionally draggable by its title bar. On `mobile` the same content
162
+ * takes over the whole screen as a page. The bucket is measured from the
163
+ * viewport by `useDeviceType`; pass `device` only to override that.
164
+ *
165
+ * Everything inside is a slot — `titleBar`, `header`, `children`, `footer` —
166
+ * which is how the Figma `idPopover` composes its variants (the component set
167
+ * declares one property, `Device`; every other difference between the usage
168
+ * examples is different content in the same slots).
169
+ *
170
+ * ```tsx
171
+ * <Popover
172
+ * open={open}
173
+ * onClose={() => setOpen(false)}
174
+ * title="Profile"
175
+ * header={<Greeting />}
176
+ * footer={<SignOut />}
177
+ * >
178
+ * {sections}
179
+ * </Popover>
180
+ * ```
181
+ */
182
+ declare const Popover: react.ForwardRefExoticComponent<PopoverProps & react.RefAttributes<HTMLDivElement>>;
183
+
184
+ /**
185
+ * The floating title row from the Figma `idPopover` spec: grab handle, title,
186
+ * pin toggle, close button.
187
+ *
188
+ * `dragHandleProps` is attached to the whole row rather than just the grip, so
189
+ * the title text is draggable too. Presses that originate inside a `<button>`
190
+ * (pin, close) are filtered out by `usePopoverDrag`, and the grip stays
191
+ * focusable so the row can also be moved with the arrow keys.
192
+ */
193
+ declare const PopoverTitleBar: react.MemoExoticComponent<({ title, draggable, dragHandleProps, pinned, onPinnedChange, onClose, testID, }: PopoverTitleBarProps) => react_jsx_runtime.JSX.Element>;
194
+
195
+ interface UsePopoverOptions {
196
+ onOpen?: () => void;
197
+ onClose?: () => void;
198
+ }
199
+ interface UsePopoverReturn {
200
+ isOpen: boolean;
201
+ open: () => void;
202
+ close: () => void;
203
+ toggle: () => void;
204
+ }
205
+ /** Open/close state for a `Popover`, mirroring `useDrawer`. */
206
+ declare function usePopover(options?: UsePopoverOptions): UsePopoverReturn;
207
+
208
+ interface UsePopoverDragOptions {
209
+ /** Drag is a no-op while this is false (mobile, or `draggable={false}`). */
210
+ enabled: boolean;
211
+ /** Starting viewport offset. `undefined` leaves the panel centred. */
212
+ initialPosition?: PopoverPosition;
213
+ onPositionChange?: (position: PopoverPosition) => void;
214
+ }
215
+ interface UsePopoverDragReturn {
216
+ /** `undefined` until the panel has been positioned, so it stays centred. */
217
+ position?: PopoverPosition;
218
+ /** Attach to the panel so the hook can measure it for clamping. */
219
+ panelRef: RefObject<HTMLDivElement>;
220
+ /** Spread onto the element that starts a drag. */
221
+ dragHandleProps: PopoverDragHandleProps;
222
+ isDragging: boolean;
223
+ }
224
+ /**
225
+ * Pointer- and keyboard-driven repositioning for a floating popover panel.
226
+ *
227
+ * The panel stays in its centred flow position until the first drag, at which
228
+ * point the hook switches it to an absolute viewport offset. Positions are
229
+ * always clamped so at least `EDGE_MARGIN` of the panel stays on screen.
230
+ */
231
+ declare const usePopoverDrag: ({ enabled, initialPosition, onPositionChange, }: UsePopoverDragOptions) => UsePopoverDragReturn;
232
+
233
+ /**
234
+ * Viewport thresholds from the Figma "Breakpoints" frame
235
+ * (file `SOOtaT80JAVJf7PKlFnt7Q`, node `6393:1137`).
236
+ */
237
+ declare const POPOVER_BREAKPOINTS: {
238
+ /** Widths at or below this are `mobile` (Figma: "Mobile: 320-768px"). */
239
+ readonly mobileMax: 768;
240
+ /** Widths at or below this, but above `mobileMax`, are `tablet` (Figma: "Tablet: 769-1023px"). */
241
+ readonly tabletMax: 1023;
242
+ };
243
+ /** Maps a viewport width in CSS pixels onto a {@link PopoverDevice} bucket. */
244
+ declare const resolveDevice: (width: number) => PopoverDevice;
245
+ /**
246
+ * Resolves the active device bucket from the viewport width, re-resolving on
247
+ * resize. Pass `override` to short-circuit measurement entirely — the override
248
+ * is returned verbatim and no listener is attached.
249
+ *
250
+ * Server-side (no `window`) the hook reports `desktop`, which matches the
251
+ * Figma default variant, and corrects itself on the first client effect.
252
+ */
253
+ declare const useDeviceType: (override?: PopoverDevice) => PopoverDevice;
254
+
255
+ export { POPOVER_BREAKPOINTS, Popover, type PopoverDevice, type PopoverDragHandleProps, type PopoverPosition, type PopoverProps, type PopoverRootProps, PopoverTitleBar, type PopoverTitleBarProps, type UsePopoverDragOptions, type UsePopoverDragReturn, type UsePopoverOptions, type UsePopoverReturn, resolveDevice, useDeviceType, usePopover, usePopoverDrag };