Web
Interactive preview
Runs through the actual Showcase web bundle, not a rewritten HTML approximation.
Overlays & feedback component recipe
Non-modal anchored surface on the shared overlay runtime with flip/shift/collision and safe-area policy.
Live Web preview
The iframe loads the real Showcase route; the source tab below is the exact typechecked fixture used for this component family.
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
AlertBanner,
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogTitle,
AlertDialogTrigger,
AppHeader,
Avatar,
Badge,
BeeThemeScope,
BottomActionBar,
Box,
Breadcrumb,
BreadcrumbItem,
Button,
Card,
Checkbox,
Chip,
ChipGroup,
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
DescriptionItem,
DescriptionList,
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogTitle,
DialogTrigger,
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
EmptyState,
ErrorState,
Field,
FormGroup,
HStack,
IconButton,
Input,
Link,
ListGroup,
ListGroupHeader,
ListItem,
OTPInput,
Pagination,
PaginationItem,
PasswordInput,
Popover,
PopoverClose,
PopoverContent,
PopoverDescription,
PopoverTitle,
PopoverTrigger,
Progress,
Radio,
RadioGroup,
SafeArea,
Screen,
SearchInput,
Section,
SegmentedControl,
SegmentedControlItem,
Separator,
SettingsItem,
Sheet,
SheetClose,
SheetContent,
SheetDescription,
SheetFooter,
SheetTitle,
SheetTrigger,
Skeleton,
Spinner,
Stack,
Stat,
StatHelpText,
StatLabel,
StatValue,
Stepper,
StepperItem,
Switch,
Tabs,
TabsContent,
TabsList,
TabsTrigger,
Text,
Textarea,
Timeline,
TimelineItem,
Tooltip,
TooltipContent,
TooltipTrigger,
useToast,
VStack,
} from '@beemvp/beeui-ui';
import * as React from 'react';
import { ScrollView, StatusBar } from 'react-native';
import { Uniwind, useUniwind } from 'uniwind';
import { DatePickerShowcase } from './date-picker-showcase';
import { DateTimePickerShowcase } from './date-time-picker-showcase';
import { SelectShowcase } from './select-showcase';
import { TableShowcase } from './table-showcase';
import { ToolbarShowcase } from './toolbar-showcase';
function ThemeToggle() {
const { hasAdaptiveThemes, theme } = useUniwind();
const activeTheme = hasAdaptiveThemes ? 'system' : theme;
const nextTheme = activeTheme === 'system' ? 'light' : activeTheme === 'light' ? 'dark' : 'system';
return (
<Button
accessibilityLabel={`Theme ${activeTheme}. Switch to ${nextTheme}`}
onPress={() => Uniwind.setTheme(nextTheme)}
size="sm"
testID="component-gallery-theme-toggle"
variant="outline"
>
{`Theme: ${activeTheme}`}
</Button>
);
}
function PlaygroundHeading({ children, description }: { children: string; description: string }) {
return (
<VStack gap="xs">
<Text variant="heading">{children}</Text>
<Text tone="muted">{description}</Text>
</VStack>
);
}
function PlacementPopover({ placement }: { placement: 'top' | 'right' | 'bottom' | 'left' }) {
return (
<Popover>
<PopoverTrigger size="sm" variant="outline">
{placement}
</PopoverTrigger>
<PopoverContent placement={placement} testID={`popover-${placement}-content`}>
<PopoverTitle>{`${placement[0].toUpperCase()}${placement.slice(1)} placement`}</PopoverTitle>
<PopoverDescription>
This surface is positioned by the shared anchored-overlay geometry kernel.
</PopoverDescription>
{placement === 'bottom' ? (
<Field label="Note">
<Input accessibilityLabel="Popover note" testID="popover-demo-input" />
</Field>
) : null}
<PopoverClose size="sm" variant="ghost">
Close
</PopoverClose>
</PopoverContent>
</Popover>
);
}
// Consumer React context declared below BeeUIProvider. Overlay content must
// resolve the provided value, not the default — the web regression asserts this.
const OverlayConsumerContext = React.createContext('overlay-context-default');
function OverlayContextValue({ testID }: { testID: string }) {
return <Text testID={testID}>{`context: ${React.useContext(OverlayConsumerContext)}`}</Text>;
}
// CASE C — scope-ordering proof for real-browser Escape. A single control opens,
// with independent open states, a root-scope Popover AND a Dialog whose nested
// DropdownMenu is also open. They live in different overlay scopes, so their
// registration order is irrelevant by construction — Escape routes to the active
// modal scope (the menu), never the root Popover behind it. (Strict "root opened
// after menu" ordering with real timing is covered deterministically in the jest
// CASE B/D contract tests; a click inside the modal cannot be used here because
// the open menu's dismiss layer intercepts pointer events.)
function CaseCScopeOrdering() {
const [dialogOpen, setDialogOpen] = React.useState(false);
const [menuOpen, setMenuOpen] = React.useState(false);
const [rootOpen, setRootOpen] = React.useState(false);
return (
<>
<Button
onPress={() => {
setDialogOpen(true);
setMenuOpen(true);
setRootOpen(true);
}}
testID="overlay-context-casec-open"
variant="outline"
>
Open CASE C
</Button>
<Popover onOpenChange={setRootOpen} open={rootOpen}>
<PopoverTrigger testID="overlay-context-casec-root-trigger" variant="outline">
CASE C root
</PopoverTrigger>
<PopoverContent placement="bottom">
<OverlayContextValue testID="overlay-context-casec-root-value" />
</PopoverContent>
</Popover>
<Dialog onOpenChange={setDialogOpen} open={dialogOpen}>
<DialogTrigger testID="overlay-context-casec-dialog-trigger">CASE C dialog</DialogTrigger>
<DialogContent>
<DialogTitle>CASE C dialog</DialogTitle>
<DropdownMenu onOpenChange={setMenuOpen} open={menuOpen}>
<DropdownMenuTrigger testID="overlay-context-casec-menu-trigger" variant="outline">
CASE C menu
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuLabel>CASE C menu</DropdownMenuLabel>
<OverlayContextValue testID="overlay-context-casec-menu-value" />
</DropdownMenuContent>
</DropdownMenu>
</DialogContent>
</Dialog>
</>
);
}
function ConsumerContextOverlays() {
const [dialogMenuAction, setDialogMenuAction] = React.useState('none');
return (
<OverlayConsumerContext.Provider value="preserved">
<VStack gap="sm">
<Popover>
<PopoverTrigger testID="overlay-context-popover-trigger" variant="outline">
Popover context
</PopoverTrigger>
<PopoverContent placement="bottom">
<PopoverTitle>Popover consumer context</PopoverTitle>
<OverlayContextValue testID="overlay-context-popover-value" />
</PopoverContent>
</Popover>
<DropdownMenu>
<DropdownMenuTrigger testID="overlay-context-menu-trigger" variant="outline">
Menu context
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuLabel>Menu consumer context</DropdownMenuLabel>
<OverlayContextValue testID="overlay-context-menu-value" />
</DropdownMenuContent>
</DropdownMenu>
<Tooltip>
<TooltipTrigger testID="overlay-context-tooltip-trigger" variant="outline">
Tooltip context
</TooltipTrigger>
<TooltipContent>
<OverlayContextValue testID="overlay-context-tooltip-value" />
</TooltipContent>
</Tooltip>
<Dialog>
<DialogTrigger testID="overlay-context-dialog-trigger">Dialog context</DialogTrigger>
<DialogContent>
<DialogTitle>Dialog with a nested overlay</DialogTitle>
<Popover>
<PopoverTrigger testID="overlay-context-dialog-popover-trigger" variant="outline">
Popover in dialog
</PopoverTrigger>
<PopoverContent placement="bottom">
<OverlayContextValue testID="overlay-context-dialog-popover-value" />
</PopoverContent>
</Popover>
<Tooltip>
<TooltipTrigger testID="overlay-context-dialog-tooltip-trigger" variant="outline">
Tooltip in dialog
</TooltipTrigger>
<TooltipContent>
<OverlayContextValue testID="overlay-context-dialog-tooltip-value" />
</TooltipContent>
</Tooltip>
<DropdownMenu>
<DropdownMenuTrigger testID="overlay-context-dialog-menu-trigger" variant="outline">
Menu in dialog
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuLabel>Menu in dialog</DropdownMenuLabel>
<OverlayContextValue testID="overlay-context-dialog-menu-value" />
<DropdownMenuItem
onSelect={() => setDialogMenuAction('selected')}
testID="overlay-context-dialog-menu-item"
>
Select in dialog
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<Text testID="overlay-context-dialog-menu-action">{`menu action: ${dialogMenuAction}`}</Text>
</DialogContent>
</Dialog>
<CaseCScopeOrdering />
</VStack>
</OverlayConsumerContext.Provider>
);
}
// #68 — real-browser proof that a BeeThemeScope declared around an overlay
// trigger is still resolved for that overlay's portaled content on web (the
// `ReactDOM.createPortal` transport). The Playwright regression
// (apps/visual-regression/tests/overlay-context.spec.ts) asserts these testIDs
// against the real Uniwind runtime, the same way issue #35 proves consumer
// context survives the same transport.
function ThemeScopeValue({ testID }: { testID: string }) {
const { theme } = useUniwind();
return <Text testID={testID}>{`theme: ${theme}`}</Text>;
}
function ThemeScopeOverlays() {
return (
<VStack gap="sm">
<BeeThemeScope appearance="dark" brand="violet">
<VStack gap="sm">
<ThemeScopeValue testID="theme-scope-root-value" />
<Popover>
<PopoverTrigger testID="theme-scope-popover-trigger" variant="outline">
Popover in scope
</PopoverTrigger>
<PopoverContent placement="bottom">
<PopoverTitle>Popover in a BeeThemeScope</PopoverTitle>
<ThemeScopeValue testID="theme-scope-popover-value" />
</PopoverContent>
</Popover>
<DropdownMenu>
<DropdownMenuTrigger testID="theme-scope-menu-trigger" variant="outline">
Menu in scope
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuLabel>Menu in a BeeThemeScope</DropdownMenuLabel>
<ThemeScopeValue testID="theme-scope-menu-value" />
</DropdownMenuContent>
</DropdownMenu>
<Dialog>
<DialogTrigger testID="theme-scope-dialog-trigger">Dialog in scope</DialogTrigger>
<DialogContent>
<DialogTitle>Dialog inside a BeeThemeScope</DialogTitle>
<ThemeScopeValue testID="theme-scope-dialog-value" />
<DialogClose size="sm" variant="ghost">
Close
</DialogClose>
</DialogContent>
</Dialog>
<BeeThemeScope appearance="light" brand="bee">
<ThemeScopeValue testID="theme-scope-nested-value" />
</BeeThemeScope>
</VStack>
</BeeThemeScope>
<ThemeScopeValue testID="theme-scope-sibling-value" />
</VStack>
);
}
function ToastPlayground() {
const toast = useToast();
const [lastAction, setLastAction] = React.useState('No Toast action yet');
return (
<Card className="gap-4" variant="raised">
<Section
description="Provider-scoped transient notifications. Up to three are visible; overflow waits FIFO. Close visible items to inspect queue promotion."
title="Toast notifications"
>
<HStack gap="sm" wrap>
<Button
onPress={() => toast.show({ title: 'Saved', description: 'Default toast with a 5 second timeout.' })}
size="sm"
variant="outline"
>
Default
</Button>
<Button
onPress={() => toast.show({ title: 'Published', description: 'Changes are live.', variant: 'success' })}
size="sm"
variant="outline"
>
Success
</Button>
<Button
onPress={() => toast.show({ title: 'Check settings', description: 'One option needs attention.', variant: 'warning' })}
size="sm"
variant="outline"
>
Warning
</Button>
<Button
onPress={() => toast.show({ title: 'Save failed', description: 'Try the operation again.', variant: 'destructive' })}
size="sm"
variant="outline"
>
Error
</Button>
<Button
onPress={() => toast.show({
title: 'Item archived',
description: 'You can undo this action.',
variant: 'info',
action: { label: 'Undo', onPress: () => setLastAction('Undo pressed') },
})}
size="sm"
variant="outline"
>
Action
</Button>
<Button
onPress={() => toast.show({ title: 'Persistent notice', description: 'Dismiss this explicitly.', duration: 'persistent', variant: 'info' })}
size="sm"
variant="outline"
>
Persistent
</Button>
<Button
onPress={() => {
for (let index = 1; index <= 6; index += 1) {
toast.show({
title: `Queued toast ${index}`,
description: index <= 3 ? 'Initially visible.' : 'Promotes FIFO after a visible toast closes.',
duration: 'persistent',
variant: index % 2 === 0 ? 'info' : 'neutral',
});
}
}}
size="sm"
variant="secondary"
>
Queue stress
</Button>
<Button onPress={toast.dismissAll} size="sm" variant="ghost">Dismiss all</Button>
</HStack>
<Text tone="muted" variant="caption">{lastAction}</Text>
</Section>
</Card>
);
}
export function ComponentGallery({ onBack }: { onBack: () => void }) {
const { theme } = useUniwind();
const [accepted, setAccepted] = React.useState(false);
const [assignedStores, setAssignedStores] = React.useState<string[]>([]);
const [notifications, setNotifications] = React.useState(true);
const [plan, setPlan] = React.useState<'starter' | 'pro'>('starter');
const [tab, setTab] = React.useState('overview');
const [otp, setOtp] = React.useState('');
const [filters, setFilters] = React.useState<string[]>(['mobile']);
const [viewMode, setViewMode] = React.useState('list');
const [page, setPage] = React.useState(2);
const [step, setStep] = React.useState(3);
const [menuToolbar, setMenuToolbar] = React.useState(true);
const [menuDensity, setMenuDensity] = React.useState('comfortable');
const [menuAction, setMenuAction] = React.useState('No action yet');
return (
<Screen testID="component-gallery">
<StatusBar barStyle={theme === 'dark' ? 'light-content' : 'dark-content'} />
<SafeArea className="bg-surface" edges={['top', 'left', 'right']} testID="component-gallery-safe-area">
<AppHeader
description="Interactive React Native component playground built entirely from the public BeeUI API."
testID="component-gallery-header"
leading={
<HStack gap="sm">
<Button
accessibilityLabel="Back to Showcase home"
testID="component-gallery-back"
onPress={onBack}
size="sm"
variant="ghost"
>
Back
</Button>
<Avatar accessibilityLabel="BeeUI" fallback="BU" />
</HStack>
}
title="Component Gallery"
trailing={<ThemeToggle />}
/>
</SafeArea>
{/*
#281 — `min-h-24` is a rem-scaled floor (matches the row/text
scaling contract, so it keeps pace at any zoom) that stops this
flex:1 scroll region from being squeezed toward zero when the
pinned AppHeader above and BottomActionBar below both grow at large
text / zoom. Without it, once combined header+bar height leaves
less room than a single row needs, `scrollIntoView` can only
partially reveal that row, and hit-testing at its (unclipped)
geometric center lands on the BottomActionBar's real on-screen
rect — reported as the bar "intercepting pointer events" for
content that's actually just been squeezed out of its own
viewport. The floor keeps the scroll region a real, usable size;
on the rare device/zoom combination where total content still
exceeds the viewport, the page falls back to ordinary
document-level scrolling instead of corrupting hit-testing for
in-view content.
*/}
<SafeArea className="flex-1 min-h-24" edges={['left', 'right']}>
<ScrollView contentContainerStyle={{ paddingBottom: 120 }}>
<Box className="mx-auto w-full max-w-3xl gap-10 px-5 py-8">
<AlertBanner
description="Everything below is rendered from the public @beemvp/beeui-ui API. Switch themes, open overlays, change form state, and resize the web window to exercise the same contracts used on native."
title="Hands-on playground"
variant="info"
/>
<PlaygroundHeading description="Buttons, semantic surfaces, feedback, and loading states.">
Foundation
</PlaygroundHeading>
<Card className="gap-4" variant="raised">
<Section
action={<IconButton accessibilityLabel="Add item" variant="outline">+</IconButton>}
description="Variants, disabled state, and loading behavior."
title="Actions"
>
<Box className="gap-3">
<Button>Primary action</Button>
<Button variant="secondary">Secondary action</Button>
<Button variant="outline">Outline action</Button>
<Button variant="ghost">Ghost action</Button>
<Button variant="destructive">Destructive action</Button>
<Button disabled>Disabled action</Button>
<Button loading>Loading action</Button>
</Box>
</Section>
</Card>
<Card className="gap-4" variant="muted">
<Text variant="heading">Status and feedback</Text>
<Box className="flex-row flex-wrap gap-2">
<Badge>Primary</Badge>
<Badge variant="secondary">Secondary</Badge>
<Badge variant="success">Success</Badge>
<Badge variant="warning">Warning</Badge>
<Badge variant="destructive">Error</Badge>
<Badge variant="info">Info</Badge>
</Box>
<Separator />
<Box className="flex-row items-center gap-5">
<Spinner testID="spinner-default" />
<Spinner tone="success" />
<Spinner tone="warning" />
<Spinner testID="spinner-destructive" tone="destructive" />
</Box>
<Progress accessibilityLabel="Profile completion" value={72} />
</Card>
<ToastPlayground />
<Card className="gap-4">
<Text variant="heading">Loading and state surfaces</Text>
<Box className="flex-row items-center gap-3">
<Skeleton className="h-12 w-12" variant="circle" />
<Box className="flex-1 gap-2">
<Skeleton className="w-2/3" variant="text" />
<Skeleton className="w-full" variant="text" />
</Box>
</Box>
<Skeleton className="h-24 w-full" />
<Separator />
<EmptyState
action={<Button size="sm">Create record</Button>}
description="Create your first record to get started."
title="No records yet"
/>
<Separator />
<ErrorState
action={<Button size="sm" variant="outline">Try again</Button>}
description="The server could not load this section."
/>
</Card>
<PlaygroundHeading description="Text-entry composition plus explicit group semantics for related choices.">
Forms
</PlaygroundHeading>
<Card className="gap-4">
<Field description="Used only for account notifications." label="Email" required testID="component-gallery-field">
<Input autoCapitalize="none" placeholder="you@example.com" />
</Field>
<Field label="Search">
<SearchInput onSearch={() => undefined} placeholder="Search projects" />
</Field>
<Field label="Password">
<PasswordInput placeholder="Enter password" />
</Field>
<Field description="Six numeric digits." label="Verification code" testID="field-otp-composition">
<OTPInput
accessibilityLabel="Verification code"
onValueChange={setOtp}
value={otp}
/>
</Field>
<Field error="Enter a valid project name." invalid label="Project name" testID="field-invalid-state">
<Input placeholder="Invalid value" />
</Field>
<Field disabled label="Managed field" testID="field-disabled-state">
<Input placeholder="Disabled by field context" />
</Field>
<Field description="Optional long-form content." label="Notes">
<Textarea placeholder="Long-form notes" />
</Field>
<Separator />
<Checkbox checked={accepted} label="Accept terms" onCheckedChange={setAccepted} />
<Checkbox
checked
disabled
label="Managed by policy"
onCheckedChange={() => undefined}
testID="checkbox-managed-state"
/>
<FormGroup
description="The group owns legend/guidance metadata while each radio stays independently discoverable."
legend="Subscription plan"
required
>
<RadioGroup
onValueChange={(value) => setPlan(value as 'starter' | 'pro')}
value={plan}
>
<Radio label="Starter plan" value="starter" />
<Radio label="Pro plan" value="pro" />
</RadioGroup>
</FormGroup>
<FormGroup
error="Select at least one store."
invalid={assignedStores.length === 0}
legend="Assign to stores"
required
testID="stores-form-group"
>
{['Store A', 'Store B', 'Store C'].map((store) => (
<Checkbox
checked={assignedStores.includes(store)}
key={store}
label={store}
onCheckedChange={(checked) =>
setAssignedStores((current) =>
checked ? [...current, store] : current.filter((value) => value !== store),
)
}
testID={`stores-checkbox-${store.replace(/\s+/g, '-').toLowerCase()}`}
/>
))}
</FormGroup>
<Box className="flex-row items-center justify-between gap-4">
<Text>Notifications</Text>
<Switch
accessibilityLabel="Notifications"
onValueChange={setNotifications}
value={notifications}
/>
</Box>
</Card>
<PlaygroundHeading description="Persistent value selection with anchored overlay transport, keyboard behavior on web, and modal-local nesting.">
Select
</PlaygroundHeading>
<SelectShowcase />
<PlaygroundHeading description="Composable primitives (no owned data/columns) — real HTML table semantics on web, keyboard-reachable sort, and caller-owned row selection.">
Table
</PlaygroundHeading>
<TableShowcase />
<PlaygroundHeading description="Field-integrated trigger; Web opens Calendar in a Popover, native delegates to the system picker.">
DatePicker
</PlaygroundHeading>
<DatePickerShowcase />
<PlaygroundHeading description="Field-integrated trigger; composes DatePicker's date part with an Input/SegmentedControl time control on Web, native delegates to the system picker (chained date+time steps on Android).">
DateTimePicker
</PlaygroundHeading>
<DateTimePickerShowcase />
<PlaygroundHeading description="Single-row toolbar that measures its own width and collapses lower-priority items into an overflow menu once the row no longer fits.">
Toolbar
</PlaygroundHeading>
<ToolbarShowcase />
<PlaygroundHeading description="Modal and anchored overlays now have real public APIs you can click through here.">
Overlay playground
</PlaygroundHeading>
<Card className="gap-5" variant="raised">
<Section
description="Centered modal-class overlays use React Native core Modal."
title="Dialog and AlertDialog"
>
<HStack gap="sm" wrap>
<Dialog>
<DialogTrigger>Open Dialog</DialogTrigger>
<DialogContent>
<DialogTitle>Project settings</DialogTitle>
<DialogDescription>
This is BeeUI's centered modal surface. Backdrop, accessibility escape, and native request-close follow the Dialog contract.
</DialogDescription>
<Field label="Project name">
<Input defaultValue="BeeUI" />
</Field>
<DialogFooter>
<DialogClose variant="outline">Cancel</DialogClose>
<DialogClose>Save changes</DialogClose>
</DialogFooter>
</DialogContent>
</Dialog>
<AlertDialog>
<AlertDialogTrigger variant="destructive">Delete project</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogTitle>Delete this project?</AlertDialogTitle>
<AlertDialogDescription>
The backdrop cannot dismiss this confirmation. Choose an explicit action instead.
</AlertDialogDescription>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction>Delete permanently</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</HStack>
</Section>
<Separator />
<Section
description="Bottom-sheet surface (#159): BeeUI's own Web overlay/focus primitives — Escape, backdrop press, Tab focus-trap, and focus restoration — with no native Modal and no gorhom on Web (ADR-006)."
title="Sheet"
>
<Sheet>
<SheetTrigger testID="sheet-demo-trigger">Open Sheet</SheetTrigger>
<SheetContent overlayTestID="sheet-demo-overlay" testID="sheet-demo-content">
<SheetTitle>Filters</SheetTitle>
<SheetDescription>Refine results by category and price.</SheetDescription>
<Field label="Search">
<Input accessibilityLabel="Sheet search" testID="sheet-demo-input" />
</Field>
<SheetFooter>
<SheetClose testID="sheet-demo-close" variant="outline">
Close
</SheetClose>
</SheetFooter>
</SheetContent>
</Sheet>
</Section>
<Separator />
<Section
description="Popover uses the shared non-Modal host, window-coordinate measurement, collision handling, and topmost dismissal stack."
title="Popover placements"
>
<HStack gap="sm" wrap>
<PlacementPopover placement="top" />
<PlacementPopover placement="right" />
<PlacementPopover placement="bottom" />
<PlacementPopover placement="left" />
</HStack>
</Section>
<Separator />
<Section
description="Menu items reuse the anchored runtime with normal, checkbox, radio, disabled, and keyboard-selection contracts."
title="DropdownMenu"
>
<VStack gap="sm">
<DropdownMenu>
<DropdownMenuTrigger variant="outline">Workspace menu</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuLabel>Workspace</DropdownMenuLabel>
<DropdownMenuItem onSelect={() => setMenuAction('Edit project')}>
Edit project
</DropdownMenuItem>
<DropdownMenuItem disabled>Archive unavailable</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuCheckboxItem
checked={menuToolbar}
onCheckedChange={setMenuToolbar}
>
Show toolbar
</DropdownMenuCheckboxItem>
<DropdownMenuSeparator />
<DropdownMenuLabel>Density</DropdownMenuLabel>
<DropdownMenuRadioGroup
onValueChange={setMenuDensity}
value={menuDensity}
>
<DropdownMenuRadioItem value="compact">Compact</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="comfortable">
Comfortable
</DropdownMenuRadioItem>
</DropdownMenuRadioGroup>
</DropdownMenuContent>
</DropdownMenu>
<Text tone="muted">
{`Last action: ${menuAction} · toolbar ${menuToolbar ? 'on' : 'off'} · ${menuDensity}`}
</Text>
</VStack>
</Section>
<Separator />
<Section
description="Non-interactive contextual disclosure (#152/#153/#154): hover or focus reveals a short description after a delay; the trigger's own press action is never intercepted, Escape dismisses without moving focus, and content is never a Tab stop."
title="Tooltip"
>
<HStack gap="sm" wrap>
<Tooltip>
<TooltipTrigger testID="tooltip-demo-trigger" variant="outline">
Autosave
</TooltipTrigger>
<TooltipContent testID="tooltip-demo-content">
Saved automatically every 30 seconds
</TooltipContent>
</Tooltip>
</HStack>
</Section>
<Separator />
<Section
description="Open the parent, then the child. Outside press / Escape / accessibility escape dismisses child-first."
title="Nested Popover"
>
<Popover>
<PopoverTrigger variant="secondary">Open parent</PopoverTrigger>
<PopoverContent align="start" placement="bottom">
<PopoverTitle>Parent Popover</PopoverTitle>
<PopoverDescription>
The child registers above this overlay in the shared dismiss stack.
</PopoverDescription>
<Popover>
<PopoverTrigger size="sm" variant="outline">Open child</PopoverTrigger>
<PopoverContent align="start" placement="right">
<PopoverTitle>Child Popover</PopoverTitle>
<PopoverDescription>
Dismiss me first; the parent remains mounted underneath.
</PopoverDescription>
<PopoverClose size="sm">Done</PopoverClose>
</PopoverContent>
</Popover>
</PopoverContent>
</Popover>
</Section>
<Separator />
<Section
description="This trigger is aligned to the container edge. Shrink the web viewport or rotate a device to make flip/shift collision handling obvious."
title="Collision edge case"
>
<Box className="items-end">
<Popover>
<PopoverTrigger variant="outline">Near right edge</PopoverTrigger>
<PopoverContent align="start" placement="right" sideOffset={12}>
<PopoverTitle>Collision-aware placement</PopoverTitle>
<PopoverDescription>
Preferred placement is right; the resolver flips or shifts only when the viewport requires it.
</PopoverDescription>
<PopoverClose size="sm" variant="ghost">Close</PopoverClose>
</PopoverContent>
</Popover>
</Box>
</Section>
<Separator />
<Section
description="Consumer React context declared below BeeUIProvider resolves inside overlay content on web, native, and inside a Dialog."
title="Consumer context"
>
<ConsumerContextOverlays />
</Section>
<Separator />
<Section
description="BeeThemeScope (#68) is a thin typed wrapper over Uniwind's own ScopedTheme. The scoped brand/appearance below resolves for Popover, DropdownMenu, and Dialog content declared inside it, nests independently of an inner scope, and never leaks to the sibling value outside it."
title="Scoped theme (BeeThemeScope)"
>
<ThemeScopeOverlays />
</Section>
</Card>
<PlaygroundHeading description="Selections, tabs, disclosure, paging, and application-level composition stay router-neutral.">
Navigation and composition
</PlaygroundHeading>
<Card className="gap-4">
<Section description="Dependency-free filters, view selection, and paging." title="Selection and navigation">
<Text variant="label">Filters</Text>
<ChipGroup
onValueChange={(value) => setFilters(Array.isArray(value) ? value : [value])}
selectionMode="multiple"
value={filters}
>
<Chip value="mobile">Mobile</Chip>
<Chip value="web">Web</Chip>
<Chip value="design">Design</Chip>
</ChipGroup>
<Text variant="label">View</Text>
<SegmentedControl onValueChange={setViewMode} value={viewMode}>
<SegmentedControlItem value="list">List</SegmentedControlItem>
<SegmentedControlItem value="grid">Grid</SegmentedControlItem>
</SegmentedControl>
<Text variant="label">Page</Text>
<Pagination onPageChange={setPage} page={page} pageCount={4}>
<PaginationItem testID="pagination-previous" type="previous" />
<PaginationItem page={1} />
<PaginationItem page={2} />
<PaginationItem page={3} />
<PaginationItem page={4} />
<PaginationItem type="next" />
</Pagination>
</Section>
</Card>
<Card className="gap-4">
<Text variant="heading">Tabs and disclosure</Text>
<Tabs onValueChange={setTab} value={tab}>
<TabsList>
<TabsTrigger value="overview">Overview</TabsTrigger>
<TabsTrigger value="details">Details</TabsTrigger>
</TabsList>
<TabsContent value="overview">
<Text tone="muted">Overview content is mounted for the active tab.</Text>
</TabsContent>
<TabsContent value="details">
<Text tone="muted">Details content is mounted only when selected.</Text>
</TabsContent>
</Tabs>
<Separator />
<Collapsible>
<CollapsibleTrigger>Advanced options</CollapsibleTrigger>
<CollapsibleContent>
<Text tone="muted">Hidden until expanded.</Text>
</CollapsibleContent>
</Collapsible>
<Accordion defaultValue="account">
<AccordionItem value="account">
<AccordionTrigger>Account</AccordionTrigger>
<AccordionContent>
<Text tone="muted">Account preferences and identity.</Text>
</AccordionContent>
</AccordionItem>
<AccordionItem value="billing">
<AccordionTrigger>Billing</AccordionTrigger>
<AccordionContent>
<Text tone="muted">Invoices and payment settings.</Text>
</AccordionContent>
</AccordionItem>
</Accordion>
</Card>
<Card className="gap-5">
<Section
description="Layout, history, and application composition without router ownership."
title="Application composition"
>
<Breadcrumb accessibilityLabel="Project breadcrumb">
<BreadcrumbItem onPress={() => undefined}>Projects</BreadcrumbItem>
<BreadcrumbItem current>BeeUI</BreadcrumbItem>
</Breadcrumb>
<Stack gap="lg">
<HStack gap="lg" wrap>
<Stat className="min-w-32 flex-1">
<StatLabel>API surface</StatLabel>
<StatValue>Public</StatValue>
<StatHelpText>Only @beemvp/beeui-ui exports</StatHelpText>
</Stat>
<Stat className="min-w-32 flex-1">
<StatLabel>Verification</StatLabel>
<StatValue>Cross-platform</StatValue>
<StatHelpText>Web, Android, and iOS gates</StatHelpText>
</Stat>
</HStack>
<Stepper currentStep={step} onStepChange={setStep} testID="stepper-showcase">
<StepperItem step={1} title="Foundation" />
<StepperItem step={2} title="Application patterns" />
<StepperItem step={3} title="Overlays" />
</Stepper>
<ListGroup>
<ListGroupHeader
description="Composition reuses existing row behavior."
title="Workspace"
/>
<ListItem
description="Portable component system"
onPress={() => undefined}
title="BeeUI"
/>
</ListGroup>
<Timeline>
<TimelineItem
description="Core layout, form, navigation, and modal contracts established."
meta="v0.1"
status="success"
title="Foundation"
/>
<TimelineItem
description="Packed packages bundle in Expo and bare React Native; Android APK compiles in CI."
meta="CI verified"
status="success"
title="Native portability"
/>
<TimelineItem
description="Public Popover and DropdownMenu exercise the shared anchored overlay geometry/runtime kernels."
meta="Current"
status="primary"
title="Anchored overlays"
/>
</Timeline>
</Stack>
</Section>
</Card>
<Card className="gap-4">
<Section description="Read-only application information patterns." title="Metadata and rows">
<DescriptionList>
<DescriptionItem label="Runtime" value="React Native 0.86.2" />
<DescriptionItem label="Styling" value="Uniwind 1.10.1" />
<DescriptionItem
description="Generated and compiled in CI"
label="Native verification"
value="Expo + bare RN"
/>
</DescriptionList>
<Separator />
<ListItem
description="Open your profile"
onPress={() => undefined}
title="Profile"
trailing={<Badge variant="success">Active</Badge>}
/>
<SettingsItem
description="Changes app appearance"
onPress={() => undefined}
title="Appearance"
value={theme}
/>
<SettingsItem
description="Native preference control"
title="Push notifications"
trailing={
<Switch
accessibilityLabel="Push notifications"
onValueChange={setNotifications}
value={notifications}
/>
}
/>
</Section>
</Card>
<VStack gap="xs">
<Text tone="muted" variant="caption">
Navigation remains application-owned. This showcase intentionally has no router or docs-site framework.
</Text>
<Link onPress={() => undefined}>Open documentation</Link>
</VStack>
</Box>
</ScrollView>
</SafeArea>
<SafeArea className="bg-surface" edges={['bottom', 'left', 'right']}>
<BottomActionBar>
<Button size="sm" variant="ghost">Cancel</Button>
<Button size="sm">Save changes</Button>
</BottomActionBar>
</SafeArea>
</Screen>
);
}
Web
Runs through the actual Showcase web bundle, not a rewritten HTML approximation.
iOS / Android
The component belongs to the same package and Showcase inventory. Platform/runtime limitations stay explicit in the component documentation.
Composition
Popover PopoverClose PopoverContent PopoverDescription PopoverTitle PopoverTrigger
Types: PopoverAlign PopoverCloseProps PopoverCollisionPadding PopoverContentProps PopoverDescriptionProps PopoverDirection PopoverPlacement PopoverProps PopoverTitleProps PopoverTriggerProps
Ownership boundary
BeeUI owns reusable UI behavior and semantics. Networking, routing, persistence, business validation, and domain side effects stay in your application.