Offline-First


SyncIndicator

Displays the current synchronisation state of the app. Maps a status value to a themed icon and label. Animates the syncing state with a rotation loop so users get clear visual feedback while data is being pushed.

PropTypeDefaultDescription
status'synced' | 'syncing' | 'pending' | 'error' | 'offline'Current sync state
label?stringautoOverride the default label for the given status
size?'sm' | 'md' | 'lg''md'Size of the icon and label text
showLabel?booleantrueWhether to render the status label beside the icon
accessibilityLabel?stringAccessibility label for the container
testID?stringTest identifier
import { SyncIndicator } from '@objectifthunes/limestone-sdk';

<SyncIndicator status="syncing" size="sm" />
<SyncIndicator status="error" label="Failed — tap to retry" />
<SyncIndicator status="synced" showLabel={false} />

ConflictCard

Renders a structured conflict-resolution UI when the same record has been mutated both locally and on the server. Presents two version panels — local and remote — with a diff-style highlight on differing fields, and two action buttons for accepting either version.

PropTypeDefaultDescription
localVersionRecord<string, unknown>The local (pending) version of the record
remoteVersionRecord<string, unknown>The server-side version of the record
fieldsConflictField[]Field descriptors: { key: string; label: string; format?: (v: unknown) => string }
onKeepLocal() => voidCalled when the user chooses the local version
onKeepRemote() => voidCalled when the user chooses the remote version
title?string'Conflict detected'Card heading
localLabel?string'Your version'Label for the local column
remoteLabel?string'Server version'Label for the remote column
variant?'default' | 'compact''default''compact' hides field labels and reduces padding
accessibilityLabel?stringAccessibility label
testID?stringTest identifier
import { ConflictCard } from '@objectifthunes/limestone-sdk';

<ConflictCard
  title="Budget conflict"
  localVersion={{ name: 'Q2 Budget', amount: 12000 }}
  remoteVersion={{ name: 'Q2 Budget', amount: 11500 }}
  fields={[
    { key: 'name', label: 'Name' },
    { key: 'amount', label: 'Amount', format: (v) => `$${v}` },
  ]}
  onKeepLocal={() => resolveConflict('local')}
  onKeepRemote={() => resolveConflict('remote')}
/>

OfflineQueueList

Visualises the queue of mutations waiting to be synced to the server. Each item displays the operation type (create / update / delete), the resource name, a relative timestamp, and an optional retry or discard action.

PropTypeDefaultDescription
itemsQueuedItem[]Array of queued mutations to display
onRetry?(id: string) => voidCalled when the user taps retry on an item
onDiscard?(id: string) => voidCalled when the user taps discard on an item
emptyMessage?string'No pending changes'Message shown when items is empty
showTimestamp?booleantrueRender relative time (e.g. “2 minutes ago”) beside each item
size?'sm' | 'md' | 'lg''md'Controls row height and font sizes
accessibilityLabel?stringAccessibility label for the list
testID?stringTest identifier

QueuedItem shape:

interface QueuedItem {
  id: string;
  operation: 'create' | 'update' | 'delete';
  resource: string;      // e.g. 'invoice', 'user', 'comment'
  label?: string;        // human-readable description of the record
  status: 'pending' | 'retrying' | 'failed';
  queuedAt: Date;
  retryCount?: number;
}
import { OfflineQueueList } from '@objectifthunes/limestone-sdk';

<OfflineQueueList
  items={pendingMutations}
  onRetry={(id) => queue.retryItem(id)}
  onDiscard={(id) => queue.discardItem(id)}
  emptyMessage="All changes synced"
/>

CachedImage

A drop-in replacement for Image that stores downloaded assets in the device’s cache directory. Falls back to a Skeleton placeholder while loading and to an error state (icon + optional message) on failure. Cache hits are served synchronously with no visible flash.

PropTypeDefaultDescription
uristringRemote URL to download and cache
cacheKey?stringSHA-256 of uriExplicit cache key; use to invalidate a specific entry
fallbackUri?stringSecondary URI to try if the primary fails
resizeMode?'cover' | 'contain' | 'stretch' | 'center''cover'Image resize mode
aspectRatio?numberAspect ratio constraint applied to the wrapper
showLoadingIndicator?booleantrueShow a Skeleton pulse while loading
errorIcon?stringIcon name from the registered set to display on error
errorMessage?stringText to show below the error icon
rounded?RadiiPropBorder radius token applied to the image
accessibilityLabel?stringAccessibility label for the image
testID?stringTest identifier
All BoxStylePropsWidth, height, margin, etc.
import { CachedImage } from '@objectifthunes/limestone-sdk';

<CachedImage
  uri="https://cdn.example.com/products/widget.jpg"
  cacheKey="product-widget-v2"
  aspectRatio={16 / 9}
  rounded="md"
  w="100%"
  errorMessage="Image unavailable"
/>

PendingBadge

An overlay badge that indicates an item has local changes not yet synced to the server. Designed to be composed on top of any other component (avatar, card, list item) via absolute positioning. Supports a numeric count for stacked pending operations and a pulsing animation while sync is in progress.

PropTypeDefaultDescription
count?numberNumber of pending operations; omit for a plain dot badge
status?'pending' | 'syncing' | 'error''pending'Controls the colour: warning / primary / destructive
pulse?booleanauto (true when status === 'syncing')Animate a pulsing ring around the badge
size?'sm' | 'md' | 'lg''md'Badge size
placement?'top-right' | 'top-left' | 'bottom-right' | 'bottom-left''top-right'Corner to anchor the badge to
childrenReact.ReactNodeThe content to overlay the badge on
accessibilityLabel?stringAccessibility label for the badge
testID?stringTest identifier
import { PendingBadge, Avatar } from '@objectifthunes/limestone-sdk';

// Plain dot — one unsaved change
<PendingBadge status="pending">
  <Avatar name="Alice Martin" />
</PendingBadge>

// Count + sync-in-progress animation
<PendingBadge count={3} status="syncing">
  <Card variant="outlined">
    <Text>Invoice #1042</Text>
  </Card>
</PendingBadge>

// Error state — sync failed
<PendingBadge status="error" placement="top-left">
  <ListItem title="Budget Q2" />
</PendingBadge>