UI Components
Build native-looking interfaces using 37 pre-styled components that match the SyncBooks design system.
Importing Components
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:
<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:
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
- Navigation & Notifications — interact with the SyncBooks host
- Components List — full reference of all components