Overlays
Modal
A full-screen overlay rendered via React Native’s Modal primitive. The headless hook returns layout and accessibility state; the component handles the native Modal wiring.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
visible | boolean | — | Controls whether the modal is open |
onClose | () => void | — | Called when the modal should close |
placement | 'center' | 'bottom' | 'center' | Content anchor position |
animationType | 'fade' | 'slide' | 'none' | 'fade' | Entry/exit animation |
backdropDismiss? | boolean | true | Tap backdrop to close |
backdropOpacity? | number | 0.5 | Dim overlay opacity (0–1) |
children | React.ReactNode | — | Modal content |
Hook: useModal
import { useModal } from '@objectifthunes/limestone-sdk';
Returns backdropStyle, contentContainerStyle, contentStyle, and accessibilityProps.
Usage
import { Modal, Stack, Text, Pressable } from '@objectifthunes/limestone-sdk';
import { useState } from 'react';
function ConfirmDelete() {
const [visible, setVisible] = useState(false);
return (
<>
<Pressable onPress={() => setVisible(true)} bg="destructive" rounded="md" px="xl" py="md" center>
<Text color="destructiveForeground">Delete account</Text>
</Pressable>
<Modal
visible={visible}
onClose={() => setVisible(false)}
placement="center"
animationType="fade"
backdropDismiss
backdropOpacity={0.6}
>
<Stack bg="card" rounded="xl" p="xl" gap="lg" w={320}>
<Text variant="heading">Delete account?</Text>
<Text variant="body" color="mutedForeground">
This action cannot be undone. All your data will be permanently removed.
</Text>
<Stack direction="horizontal" gap="md" justify="flex-end">
<Pressable onPress={() => setVisible(false)} bg="muted" rounded="md" px="lg" py="sm" center>
<Text variant="label">Cancel</Text>
</Pressable>
<Pressable onPress={handleDelete} bg="destructive" rounded="md" px="lg" py="sm" center>
<Text color="destructiveForeground" variant="label">Delete</Text>
</Pressable>
</Stack>
</Stack>
</Modal>
</>
);
}
Bottom sheet style
Use placement="bottom" with animationType="slide" for a sheet-like appearance.
<Modal
visible={visible}
onClose={() => setVisible(false)}
placement="bottom"
animationType="slide"
>
<Stack bg="card" rounded="xl" p="xl" gap="md">
<Text variant="heading">Share</Text>
{/* share options */}
</Stack>
</Modal>
Popover
A floating panel anchored to a specific element on screen. The caller measures the anchor element and passes its layout rect; the hook computes position and arrow offset automatically.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
visible | boolean | — | Controls whether the popover is shown |
onClose | () => void | — | Called when the popover should close |
anchorRect | LayoutRect | — | Bounding box of the anchor element ({ x, y, width, height }) |
placement? | 'top' | 'bottom' | 'left' | 'right' | 'bottom' | Preferred side relative to the anchor |
showArrow? | boolean | true | Render a triangular pointer toward the anchor |
backdropDismiss? | boolean | true | Tap outside to close |
children | React.ReactNode | — | Popover content |
Hook: usePopover
import { usePopover } from '@objectifthunes/limestone-sdk';
Returns popoverStyle, arrowStyle, backdropStyle, and accessibilityProps.
Usage
import { Popover, Stack, Text, Pressable } from '@objectifthunes/limestone-sdk';
import { useRef, useState } from 'react';
import type { LayoutRect } from '@objectifthunes/limestone-sdk';
function InfoPopover() {
const [visible, setVisible] = useState(false);
const [anchor, setAnchor] = useState<LayoutRect>({ x: 0, y: 0, width: 0, height: 0 });
return (
<>
<Pressable
onPress={() => setVisible(true)}
onLayout={(e) => {
e.target.measure((x, y, width, height, pageX, pageY) => {
setAnchor({ x: pageX, y: pageY, width, height });
});
}}
p="sm"
>
<Text>Info</Text>
</Pressable>
<Popover
visible={visible}
onClose={() => setVisible(false)}
anchorRect={anchor}
placement="bottom"
showArrow
>
<Stack p="md" gap="xs" w={200}>
<Text variant="label">About this feature</Text>
<Text variant="body" color="mutedForeground">
Tap anywhere outside to dismiss.
</Text>
</Stack>
</Popover>
</>
);
}
Tooltip
A lightweight single-line label that appears above or below an element. Unlike Popover, Tooltip has no close button — it dismisses on tap-outside or after a configurable delay.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
visible | boolean | — | Controls whether the tooltip is shown |
onClose | () => void | — | Called when the tooltip should close |
text | string | — | Tooltip label text |
anchorRect | LayoutRect | — | Bounding box of the anchor element |
placement? | 'top' | 'bottom' | 'top' | Side relative to the anchor |
autoHideMs? | number | — | Auto-dismiss after N milliseconds (omit to keep visible until onClose) |
Hook: useTooltip
import { useTooltip } from '@objectifthunes/limestone-sdk';
Returns tooltipStyle, textStyle, arrowStyle, and accessibilityProps.
Usage
import { Tooltip, Pressable, Text } from '@objectifthunes/limestone-sdk';
import { useState } from 'react';
import type { LayoutRect } from '@objectifthunes/limestone-sdk';
function CopyButton({ value }: { value: string }) {
const [visible, setVisible] = useState(false);
const [anchor, setAnchor] = useState<LayoutRect>({ x: 0, y: 0, width: 0, height: 0 });
const handleCopy = () => {
clipboard.setString(value);
setVisible(true);
};
return (
<>
<Pressable
onPress={handleCopy}
onLayout={(e) => {
e.target.measure((x, y, width, height, pageX, pageY) => {
setAnchor({ x: pageX, y: pageY, width, height });
});
}}
p="sm"
>
<Text>Copy</Text>
</Pressable>
<Tooltip
visible={visible}
onClose={() => setVisible(false)}
text="Copied!"
anchorRect={anchor}
placement="top"
autoHideMs={1500}
/>
</>
);
}
Menu
A positioned dropdown list of labelled actions. Typically anchored to a three-dot icon or similar trigger. Items support icons, a disabled state, and a destructive style.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
visible | boolean | — | Controls whether the menu is shown |
onClose | () => void | — | Called when the menu should close |
items | MenuItem[] | — | Menu item descriptors (see below) |
anchorRect | LayoutRect | — | Bounding box of the trigger element |
placement? | 'top' | 'bottom' | 'left' | 'right' | 'bottom' | Preferred side relative to the anchor |
backdropDismiss? | boolean | true | Tap outside to close |
MenuItem shape:
interface MenuItem {
key: string;
label: string;
icon?: string;
disabled?: boolean;
destructive?: boolean;
onPress: () => void;
}
Hook: useMenu
import { useMenu } from '@objectifthunes/limestone-sdk';
Returns menuStyle, itemStyle, per-item labelStyle, iconStyle, and accessibilityProps.
Usage
import { Menu, Pressable, Icon } from '@objectifthunes/limestone-sdk';
import { useState } from 'react';
import type { LayoutRect } from '@objectifthunes/limestone-sdk';
function PostMenu({ postId }: { postId: string }) {
const [visible, setVisible] = useState(false);
const [anchor, setAnchor] = useState<LayoutRect>({ x: 0, y: 0, width: 0, height: 0 });
return (
<>
<Pressable
onPress={() => setVisible(true)}
onLayout={(e) => {
e.target.measure((x, y, width, height, pageX, pageY) => {
setAnchor({ x: pageX, y: pageY, width, height });
});
}}
p="sm"
>
<Icon name="more-vertical" size="md" />
</Pressable>
<Menu
visible={visible}
onClose={() => setVisible(false)}
anchorRect={anchor}
placement="bottom"
items={[
{ key: 'edit', label: 'Edit', icon: 'edit', onPress: () => editPost(postId) },
{ key: 'pin', label: 'Pin', icon: 'pin', onPress: () => pinPost(postId) },
{ key: 'delete', label: 'Delete', icon: 'trash', destructive: true, onPress: () => deletePost(postId) },
]}
/>
</>
);
}
Lightbox
A full-screen image viewer with swipe-between-images support, a close button, and an optional image counter. Pass an array of { uri, alt } objects and the index to open at.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
visible | boolean | — | Controls whether the lightbox is open |
onClose | () => void | — | Called when the lightbox should close |
images | LightboxImage[] | — | Image descriptors (see below) |
initialIndex? | number | 0 | Index of the image to show first |
showCounter? | boolean | true | Show 1 / N counter at the top |
LightboxImage shape:
interface LightboxImage {
uri: string;
alt?: string;
}
Hook: useLightbox
import { useLightbox } from '@objectifthunes/limestone-sdk';
Returns containerStyle, imageStyle, counterStyle, closeButtonStyle, and accessibilityProps.
Usage
import { Lightbox, Image, Pressable } from '@objectifthunes/limestone-sdk';
import { useState } from 'react';
const PHOTOS = [
{ uri: 'https://example.com/photo-1.jpg', alt: 'Mountain at sunrise' },
{ uri: 'https://example.com/photo-2.jpg', alt: 'Valley in fog' },
{ uri: 'https://example.com/photo-3.jpg', alt: 'Lake reflection' },
];
function PhotoGallery() {
const [lightboxIndex, setLightboxIndex] = useState<number | null>(null);
return (
<>
{PHOTOS.map((photo, i) => (
<Pressable key={photo.uri} onPress={() => setLightboxIndex(i)}>
<Image src={photo.uri} alt={photo.alt} w={120} h={120} rounded="md" />
</Pressable>
))}
<Lightbox
visible={lightboxIndex !== null}
onClose={() => setLightboxIndex(null)}
images={PHOTOS}
initialIndex={lightboxIndex ?? 0}
showCounter
/>
</>
);
}
ContextMenu
A long-press triggered menu that floats near the pressed element. On iOS 14+ it uses the native UIContextMenu; on Android and older iOS it falls back to the positioned Menu component.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
items | MenuItem[] | — | Menu item descriptors — same shape as Menu |
title? | string | — | Preview title shown above the items (iOS only) |
disabled? | boolean | false | Disables the long-press gesture |
previewComponent? | React.ReactNode | — | Custom preview rendered above the menu (iOS only) |
children | React.ReactNode | — | The element the context menu wraps |
accessibilityLabel? | string | — | Accessibility label |
testID? | string | — | Test identifier |
MenuItem shape is identical to the one used by Menu:
interface MenuItem {
key: string;
label: string;
icon?: string;
disabled?: boolean;
destructive?: boolean;
onPress: () => void;
}
Hook: useContextMenu
import { useContextMenu } from '@objectifthunes/limestone-sdk';
Returns gestureProps to spread on the trigger element, and menuVisible / setMenuVisible for fallback positioning.
Usage
import { ContextMenu, Image } from '@objectifthunes/limestone-sdk';
function PhotoCard({ photo }: { photo: { uri: string; id: string } }) {
return (
<ContextMenu
title="Photo options"
items={[
{ key: 'save', label: 'Save to library', icon: 'download', onPress: () => savePhoto(photo.id) },
{ key: 'share', label: 'Share', icon: 'share', onPress: () => sharePhoto(photo.id) },
{ key: 'delete', label: 'Delete', icon: 'trash', destructive: true, onPress: () => deletePhoto(photo.id) },
]}
>
<Image src={photo.uri} alt="Photo" w={160} h={160} rounded="md" />
</ContextMenu>
);
}