Overlays


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

PropTypeDefaultDescription
visiblebooleanControls whether the modal is open
onClose() => voidCalled when the modal should close
placement'center' | 'bottom''center'Content anchor position
animationType'fade' | 'slide' | 'none''fade'Entry/exit animation
backdropDismiss?booleantrueTap backdrop to close
backdropOpacity?number0.5Dim overlay opacity (0–1)
childrenReact.ReactNodeModal 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

PropTypeDefaultDescription
visiblebooleanControls whether the popover is shown
onClose() => voidCalled when the popover should close
anchorRectLayoutRectBounding box of the anchor element ({ x, y, width, height })
placement?'top' | 'bottom' | 'left' | 'right''bottom'Preferred side relative to the anchor
showArrow?booleantrueRender a triangular pointer toward the anchor
backdropDismiss?booleantrueTap outside to close
childrenReact.ReactNodePopover 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

PropTypeDefaultDescription
visiblebooleanControls whether the tooltip is shown
onClose() => voidCalled when the tooltip should close
textstringTooltip label text
anchorRectLayoutRectBounding box of the anchor element
placement?'top' | 'bottom''top'Side relative to the anchor
autoHideMs?numberAuto-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}
      />
    </>
  );
}

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

PropTypeDefaultDescription
visiblebooleanControls whether the menu is shown
onClose() => voidCalled when the menu should close
itemsMenuItem[]Menu item descriptors (see below)
anchorRectLayoutRectBounding box of the trigger element
placement?'top' | 'bottom' | 'left' | 'right''bottom'Preferred side relative to the anchor
backdropDismiss?booleantrueTap 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) },
        ]}
      />
    </>
  );
}

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

PropTypeDefaultDescription
visiblebooleanControls whether the lightbox is open
onClose() => voidCalled when the lightbox should close
imagesLightboxImage[]Image descriptors (see below)
initialIndex?number0Index of the image to show first
showCounter?booleantrueShow 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

PropTypeDefaultDescription
itemsMenuItem[]Menu item descriptors — same shape as Menu
title?stringPreview title shown above the items (iOS only)
disabled?booleanfalseDisables the long-press gesture
previewComponent?React.ReactNodeCustom preview rendered above the menu (iOS only)
childrenReact.ReactNodeThe element the context menu wraps
accessibilityLabel?stringAccessibility label
testID?stringTest 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>
  );
}