/
Documentation

UI Components

Build native-looking interfaces using 37 pre-styled components that match the SyncBooks design system.

Importing Components

example.tsx
import {
  // Layout
  PageLayout, Card, CardContent, CardHeader, CardTitle, Tabs, Divider, Drawer,

  // Forms
  Button, Input, Textarea, Select, Checkbox, Switch,
  RadioGroup, SearchInput, CurrencyInput, DateInput, FileInput,

  // Data Display
  DataTable, Table, TableHeader, TableBody, TableRow, TableHead, TableCell,
  StatCard, Badge, StatusBadge, Avatar, Progress, Pagination, List, ListItem,

  // Feedback
  Alert, Spinner, Skeleton, SkeletonRow, SkeletonCard,
  EmptyState, ErrorBoundary, LoadingOverlay, Tooltip,

  // Overlays
  Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter,
  ConfirmDialog,

  // Smart Components
  ResourcePicker, PermissionGate, Form,
} from '@syncbooks/addon-sdk/components'

PageLayout

Standard page structure with title, description, and action buttons:

Dashboard.tsx
<PageLayout
  title="Invoice Analytics"
  description="Track payment patterns and revenue trends"
  actions={
    <>
      <Button variant="outline" onClick={handleExport}>Export</Button>
      <Button onClick={handleCreate}>New Invoice</Button>
    </>
  }
>
  {/* Your page content */}
  <StatCards />
  <InvoiceTable />
</PageLayout>

DataTable

The most common pattern — a full-featured table with search, pagination, loading, and empty states built in:

CustomerTable.tsx
import { DataTable, StatusBadge } from '@syncbooks/addon-sdk/components'
import { useCustomers } from '@syncbooks/addon-sdk'

function CustomerTable() {
  const { data, loading, total, page, totalPages, setPage } = useCustomers({ limit: 20 })
  const [search, setSearch] = useState('')

  return (
    <DataTable
      columns={[
        { key: 'name', header: 'Customer Name' },
        { key: 'email', header: 'Email' },
        { key: 'phone', header: 'Phone', width: '150px' },
        {
          key: 'outstandingBalance',
          header: 'Balance',
          align: 'right',
          render: (val) => `GHS ${(val || 0).toFixed(2)}`,
        },
        {
          key: 'status',
          header: 'Status',
          render: (val) => <StatusBadge status={val || 'active'} />,
        },
      ]}
      data={data}
      loading={loading}
      total={total}
      page={page}
      totalPages={totalPages}
      onPageChange={setPage}
      onSearch={setSearch}
      searchPlaceholder="Search customers..."
      emptyTitle="No customers found"
      emptyDescription="Try a different search or create your first customer."
      onRowClick={(row) => setSelectedCustomer(row._id)}
      rowKey="_id"
    />
  )
}

StatCard

Dashboard metric cards with optional trend indicators:

<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
  <StatCard label="Revenue" value="GH₵ 45,200" trend={12.5} />
  <StatCard label="Expenses" value="GH₵ 18,900" trend={-3.2} />
  <StatCard label="Overdue" value={8} subtext="3 critical" />
  <StatCard label="Customers" value={342} trend={5.1} subtext="+12 this month" />
</div>

ResourcePicker

A searchable dropdown that fetches records from the SyncBooks API:

import { ResourcePicker } from '@syncbooks/addon-sdk/components'

function InvoiceForm() {
  const [customerId, setCustomerId] = useState('')

  return (
    <div>
      <ResourcePicker
        resource="customers"
        value={customerId}
        onChange={(id, customer) => {
          setCustomerId(id)
          console.log('Selected:', customer.name)
        }}
        label="Customer"
        placeholder="Search and select a customer..."
      />

      {/* Also works with: products, invoices, vendors, employees, projects */}
      <ResourcePicker resource="products" onChange={handleProductSelect} label="Product" />
    </div>
  )
}

PermissionGate

Conditionally render UI based on granted scopes:

import { PermissionGate } from '@syncbooks/addon-sdk/components'

function InvoiceActions({ invoiceId }) {
  return (
    <div className="flex gap-2">
      {/* Always visible if invoices.read is granted */}
      <Button variant="outline">View Details</Button>

      {/* Only visible if invoices.write is granted */}
      <PermissionGate scope="invoices.write">
        <Button>Edit Invoice</Button>
      </PermissionGate>

      {/* Only visible if invoices.delete is granted */}
      <PermissionGate scope="invoices.delete" fallback={<Tooltip content="You need delete permission"><Button disabled>Delete</Button></Tooltip>}>
        <Button variant="destructive">Delete</Button>
      </PermissionGate>

      {/* Require multiple scopes */}
      <PermissionGate scope={["invoices.write", "payments.write"]}>
        <Button>Record Payment</Button>
      </PermissionGate>
    </div>
  )
}

Form

Declarative form with built-in validation and submit handling:

import { Form, Input, CurrencyInput, Select, Button } from '@syncbooks/addon-sdk/components'
import { customers, ui } from '@syncbooks/addon-sdk'

function CreateCustomerForm() {
  return (
    <Form
      initialValues={{ name: '', email: '', phone: '', type: 'individual' }}
      validate={(values) => ({
        ...(!values.name && { name: 'Name is required' }),
        ...(!values.email && { email: 'Email is required' }),
        ...(values.email && !values.email.includes('@') && { email: 'Invalid email' }),
      })}
      onSubmit={async (values) => {
        await customers.create(values)
        ui.notify('Customer created!', 'success')
      }}
    >
      {({ values, errors, setValue, loading }) => (
        <div className="space-y-4">
          <Input
            label="Customer Name"
            value={values.name}
            onChange={(e) => setValue('name', e.target.value)}
            error={errors.name}
            placeholder="Enter full name or company"
          />
          <Input
            label="Email"
            type="email"
            value={values.email}
            onChange={(e) => setValue('email', e.target.value)}
            error={errors.email}
          />
          <Input
            label="Phone"
            value={values.phone}
            onChange={(e) => setValue('phone', e.target.value)}
          />
          <Select
            label="Type"
            value={values.type}
            onChange={(e) => setValue('type', e.target.value)}
            options={[
              { value: 'individual', label: 'Individual' },
              { value: 'business', label: 'Business' },
            ]}
          />
          <Button type="submit" loading={loading}>Create Customer</Button>
        </div>
      )}
    </Form>
  )
}

Drawer

A slide-in panel for detail views and forms:

import { Drawer, Button } from '@syncbooks/addon-sdk/components'

function App() {
  const [showDetail, setShowDetail] = useState(false)
  const [selectedId, setSelectedId] = useState(null)

  return (
    <>
      <DataTable onRowClick={(row) => { setSelectedId(row._id); setShowDetail(true) }} ... />

      <Drawer
        open={showDetail}
        onClose={() => setShowDetail(false)}
        title="Customer Details"
        side="right"
        width="450px"
        footer={
          <>
            <Button variant="outline" onClick={() => setShowDetail(false)}>Close</Button>
            <Button>Save Changes</Button>
          </>
        }
      >
        {selectedId && <CustomerDetail id={selectedId} />}
      </Drawer>
    </>
  )
}

ConfirmDialog

For destructive or important actions:

import { ConfirmDialog, Button } from '@syncbooks/addon-sdk/components'

function DeleteButton({ customerId, customerName }) {
  const [showConfirm, setShowConfirm] = useState(false)
  const [deleting, setDeleting] = useState(false)

  async function handleDelete() {
    setDeleting(true)
    await customers.delete(customerId)
    ui.notify('Customer deleted', 'success')
    setDeleting(false)
    setShowConfirm(false)
  }

  return (
    <>
      <Button variant="destructive" onClick={() => setShowConfirm(true)}>Delete</Button>
      <ConfirmDialog
        open={showConfirm}
        onCancel={() => setShowConfirm(false)}
        onConfirm={handleDelete}
        title="Delete Customer"
        description={`Are you sure you want to delete "${customerName}"? This cannot be undone.`}
        confirmLabel="Delete Forever"
        variant="destructive"
        loading={deleting}
      />
    </>
  )
}

Alert

<Alert type="info" title="Pro Tip">
  <p>Use the search parameter to filter large datasets server-side instead of loading everything.</p>
</Alert>

<Alert type="warning" title="Rate Limit Warning">
  <p>You're approaching the 1,000 requests/hour limit. Consider caching data locally.</p>
</Alert>

<Alert type="danger" title="Destructive Action">
  <p>Deleting this record will remove all associated data permanently.</p>
</Alert>

Complete Component List

See the Components List reference for all 37 components with their props.

Match SyncBooks styling automatically

All SDK components use the same colors, spacing, and typography as SyncBooks. Your add-on will look native without any extra CSS work.

Next Steps