Components
Import every component from @olan-app/sdk. These 23 are the only ones that exist (SDK 1.5 added
Tabs, Switch, MultiSelect, Stat, Banner, Image, Avatar and ProgressBar; apps using
them declare "sdk": "^1.5.0"); Olan draws each
with its own native control (web, desktop, iOS, Android). All props are optional unless marked
required. Props not listed here are rejected (tree.unknown_prop), including style,
className, color and id.
Handler props (onPress, onSelect, onChange, onSubmit, …) may be async; a rejected promise is
reported as an app error. Event payloads are listed with each handler.
Shared value sets:
| Type | Values |
|---|---|
Tone |
'default' | 'muted' | 'accent' | 'danger' | 'success' | 'warning' |
Spacing |
'none' | 'xs' | 'sm' | 'md' | 'lg' |
Key |
string | number (non-empty string) |
IconName |
a name from the icon list at the end of this page |
Action |
{ key: Key; label: string; icon?: IconName; onPress: () => void } |
Children: Screen, Stack, Card and Form take any components; Text takes only text; every
other component takes no children (tree.children_not_allowed).
Screen #
Title bar plus body. Required root of the app and settings surfaces (validate --strict
warns ui.root_not_screen); also the recommended root of modal and panel surfaces (chat.*,
mail.*, calendar.event.action, meeting.panel, SDK 1.7 task.action, issue.action, record.card), where its title and actions are the header.
project.widget uses Card (or EmptyState) instead. project.tab and project.settings (SDK 1.6)
also start with Screen: its title heads the tab or the settings sheet.
| Prop | Type | Notes |
|---|---|---|
title |
string |
Title bar text |
subtitle |
string |
|
actions |
Action[] |
Buttons in the title bar |
loading |
boolean |
Shows a loading indicator |
scroll |
boolean |
Body scrolls (default: host decides) |
| children | any |
<Screen title="Projeler" loading={q.loading} actions={[{ key: 'refresh', label: 'Yenile', icon: 'refresh', onPress: () => q.refetch() }]}>
…
</Screen>
Stack #
Lays children out in a column (default) or a row.
| Prop | Type |
|---|---|
direction |
'vertical' | 'horizontal' |
gap |
Spacing |
align |
'start' | 'center' | 'end' | 'stretch' |
justify |
'start' | 'center' | 'end' | 'between' |
wrap |
boolean |
padding |
Spacing |
| children | any |
Card #
A grouped block. The usual root of a project.widget surface.
| Prop | Type |
|---|---|
title |
string |
subtitle |
string |
actions |
Action[] |
padding |
Spacing |
| children | any |
Text #
| Prop | Type | Notes |
|---|---|---|
text |
string |
Or pass the text as children: <Text>Merhaba {name}</Text> |
variant |
'title' | 'heading' | 'body' | 'caption' | 'label' |
|
tone |
Tone |
|
weight |
'regular' | 'medium' | 'semibold' | 'bold' |
|
truncate |
boolean |
One line with an ellipsis |
Children may only be strings, numbers, booleans/null (ignored) or arrays of them — no elements.
Badge #
| Prop | Type |
|---|---|
text required |
string |
tone |
Tone |
EmptyState #
| Prop | Type | Notes |
|---|---|---|
title required |
string |
|
description |
string |
|
icon |
IconName |
|
action |
{ label: string; onPress: () => void } |
A button under the text |
List #
Rows with a title. Send all items; the host virtualises long lists.
| Prop | Type | Notes |
|---|---|---|
items required |
ListItem[] |
|
onSelect |
({ key }) => void |
Row pressed; key is the item's key |
emptyText |
string |
Shown when items is empty |
loading |
boolean |
ListItem: { key: Key (required); title: string (required); subtitle?: string; meta?: string (short trailing text, e.g. a date); badge?: { text: string; tone?: Tone }; icon?: IconName; disabled?: boolean }.
<List
items={tasks.map((t) => ({ key: t.id, title: t.title, meta: t.due ?? undefined, badge: { text: 'Gecikti', tone: 'danger' } }))}
emptyText="Görev yok"
onSelect={({ key }) => navigation.push('task', { taskId: String(key) })}
/>
Do not pass undefined-valued text you need to see: meta: t.due ?? undefined omits it cleanly.
Table #
| Prop | Type | Notes |
|---|---|---|
columns required |
TableColumn[] |
below |
rows required |
{ key: Key; cells: Record<columnKey, string | number | null> }[] |
|
onSelectRow |
({ key }) => void |
|
emptyText |
string |
|
defaultSort |
{ key: string; direction: 'asc' | 'desc' } |
Initial order (SDK 1.5) |
TableColumn: { key: string; title: string; width?: number; align?: 'start' | 'center' | 'end'; format?: 'text' | 'number' | 'currency' | 'percent'; currency?: string; sortable?: boolean }.
width is a relative (flex) weight > 0. SDK 1.5:
format: the host formats number cells with the person's locale (Intl.NumberFormat); a cell that is not a number shows as it is.percenttakes a fraction (0.034→ "%3,4").currency: ISO 4217 ('TRY'), required withformat: 'currency'(tree.invalid_prop).sortable: the person sorts by tapping the title. The host sorts the rows itself, on the raw cell values (numbers as numbers); there is no handler.
<Table
columns={[
{ key: 'name', title: 'Kampanya', sortable: true },
{ key: 'spend', title: 'Harcama', format: 'currency', currency: 'TRY', align: 'end', sortable: true },
{ key: 'ctr', title: 'TO', format: 'percent', align: 'end', sortable: true },
]}
rows={campaigns.map((c) => ({ key: c.id, cells: { name: c.name, spend: c.spend, ctr: c.ctr } }))}
defaultSort={{ key: 'spend', direction: 'desc' }}
/>
Chart #
| Prop | Type | Notes |
|---|---|---|
kind required |
'bar' | 'line' | 'area' |
area since SDK 1.5 |
series required |
{ name: string; points: { x: string; y: number }[] }[] |
x is a label (e.g. a date) |
height |
number |
Integer 1–2000 |
stacked |
boolean |
Stacks the series; bar and area only (SDK 1.5) |
valueFormat |
'number' | 'currency' | 'percent' |
Axis and tooltip format (SDK 1.5; percent takes fractions) |
currency |
string |
ISO 4217; required with valueFormat: 'currency' |
Colours come from Olan's chart palette; you cannot set them.
<Chart kind="area" stacked valueFormat="currency" currency="TRY"
series={[{ name: 'Meta', points: days.map((d) => ({ x: d.day, y: d.meta })) }, { name: 'Google', points: days.map((d) => ({ x: d.day, y: d.google })) }]} />
Form #
Groups fields and submits their values together.
| Prop | Type | Notes |
|---|---|---|
onSubmit required |
({ values }) => void |
values: Record<string, string> maps each field name to its value |
submitLabel |
string |
Submit button text |
cancelLabel |
string |
|
onCancel |
() => void |
|
| children | any | Fields and layout |
<Form submitLabel="Kaydet" onSubmit={({ values }) => save(values.title, values.due)}>
<TextField name="title" label="Başlık" required />
<DatePicker name="due" label="Bitiş" />
</Form>
Submitting does not clear the fields: an uncontrolled field (no value) keeps what the user
typed, in Olan and in renderApp. To empty the form after a save, either control the fields
(value={title} + onChange={({ value }) => setTitle(value)}, then setTitle('')) or remount
the form with a new React key (<Form key={saved} …>, setSaved((n) => n + 1)).
TextField #
| Prop | Type | Notes |
|---|---|---|
name required |
string |
Key in Form values |
label required |
string |
|
value |
string |
Controlled value; omit for an uncontrolled field (it is not cleared on submit, see Form) |
placeholder |
string |
|
multiline |
boolean |
|
secure |
boolean |
Password entry |
required |
boolean |
|
error |
string |
Validation message under the field |
onChange |
({ value }) => void |
value: string |
maxLength |
number |
1–100000 (SDK 1.5): the host stops input at this many characters and shows an "n/max" counter under the field. Counted in Unicode code points ([...value].length), so an emoji is one |
Select #
The platform's own picker.
| Prop | Type | Notes |
|---|---|---|
name required |
string |
|
label required |
string |
|
options required |
{ value: string; label: string }[] |
|
value |
string | null |
Selected option value; null = none |
placeholder |
string |
|
onChange |
({ value }) => void |
value is the option value |
DatePicker #
The platform's own date (or date and time) picker.
| Prop | Type | Notes |
|---|---|---|
name required |
string |
|
label required |
string |
|
value |
string | null |
ISO date YYYY-MM-DD or ISO date-time |
mode |
'date' | 'datetime' |
|
onChange |
({ value }) => void |
ISO string |
Button #
| Prop | Type | Notes |
|---|---|---|
label required |
string |
Never empty (ui.button_label_missing) |
onPress required |
() => void |
|
variant |
'primary' | 'secondary' | 'ghost' | 'danger' |
|
icon |
IconName |
Shown next to the label |
disabled |
boolean |
|
loading |
boolean |
Also blocks presses |
Menu #
An anchored menu button: UIMenu on iOS, PopupMenu on Android, the Olan menu on web.
| Prop | Type | Notes |
|---|---|---|
label |
string |
Button text. label or icon is required; prefer a label |
icon |
IconName |
|
items required |
{ key: Key; label: string; icon?: IconName; destructive?: boolean; disabled?: boolean }[] |
|
onSelect required |
({ key }) => void |
Chosen item |
<Menu
label="Durumu değiştir"
items={[{ key: 'in-process', label: 'Sürüyor' }, { key: 'completed', label: 'Tamamlandı' }, { key: 'cancelled', label: 'İptal', destructive: true }]}
onSelect={({ key }) => setStatus(String(key) as ProjectTaskStatus)}
/>
For a menu opened from code (not anchored to a button) use olan.ui.menu (api.md).
SDK 1.5 components #
The components below need "sdk": "^1.5.0" and an Olan server that knows them (an older one
reports tree.unknown_type). They take no children and are not allowed in chat cards
(card.component_not_allowed). Numbers are formatted by the host with the person's locale
(Intl.NumberFormat); percent always takes a fraction (0.034 → "%3,4" in Turkish).
Tabs #
A controlled segmented control: UISegmentedControl on iOS, Material segmented buttons on Android (a scrolling tab bar above 5 tabs), the Olan segmented control on web. Render the selected content yourself.
| Prop | Type | Notes |
|---|---|---|
items required |
{ key: Key; label: string; badge?: string | number; disabled?: boolean }[] |
2–8 tabs; label ≤ 40, never empty (a11y.tab_label_missing); badge ≤ 16 characters or a count |
value required |
Key |
The selected key |
label |
string |
The tab group's accessible name for screen readers, not shown; ≤ 40, default "Bölümler". Leave it out rather than passing an empty string (a11y.tab_label_missing) |
onChange required |
({ key }) => void |
const [tab, setTab] = useState('media');
<Tabs value={tab} label="Medya bölümleri" onChange={({ key }) => setTab(String(key))}
items={[{ key: 'media', label: 'Medya', badge: processing }, { key: 'upload', label: 'Yükle' }, { key: 'stats', label: 'Analiz' }]} />
{tab === 'media' ? <MediaList /> : tab === 'upload' ? <UploadForm /> : <Stats />}
Switch #
The platform's on/off switch (UISwitch, Material switch).
| Prop | Type | Notes |
|---|---|---|
label required |
string |
Never empty (a11y.field_label_missing) |
value required |
boolean |
|
description |
string |
A second line under the label |
disabled |
boolean |
|
onChange |
({ value }) => void |
value: boolean, the new state |
Not a Form field: its value is not in onSubmit's values (still Record<string, string>);
keep it in state with onChange.
MultiSelect #
Several values from a list: a checkmarked native menu on iOS (it stays open while the person ticks), a multi-choice dialog on Android, the Olan menu on web. The trigger shows how many are selected.
| Prop | Type | Notes |
|---|---|---|
label required |
string |
Never empty (a11y.field_label_missing) |
value required |
string[] |
The selected option values |
options required |
{ value: string; label: string }[] |
≤ 200 |
placeholder |
string |
Shown when nothing is selected |
max |
number |
Most values the person may select |
disabled |
boolean |
|
onChange |
({ value }) => void |
value: string[], the new selection |
Not a Form field either: keep its value with onChange.
<MultiSelect label="Kanallar" value={channels} onChange={({ value }) => setChannels(value)} max={3}
options={[{ value: 'instagram', label: 'Instagram' }, { value: 'tiktok', label: 'TikTok' }, { value: 'linkedin', label: 'LinkedIn' }]} />
Stat #
One key figure (a KPI tile).
| Prop | Type | Notes |
|---|---|---|
label required |
string |
|
value required |
number | string | null |
A number is formatted by format; a string shows as it is; null shows "—" |
format |
'number' | 'currency' | 'percent' |
Default number |
currency |
string |
ISO 4217 ('TRY'); required with format: 'currency' (tree.invalid_prop) |
delta |
number |
Relative change as a fraction, shown as a signed percent (0.124 → "+%12,4") |
deltaGood |
'up' | 'down' |
Which direction is good (green); default up, down for costs such as CPC |
caption |
string |
A short line, e.g. "Son 30 gün" |
<Stack direction="horizontal" gap="md" wrap>
<Stat label="Harcama" value={1250.5} format="currency" currency="TRY" delta={0.08} caption="Son 7 gün" />
<Stat label="Tıklama oranı" value={0.034} format="percent" delta={-0.012} />
<Stat label="Tıklama başı maliyet" value={2.4} format="currency" currency="TRY" delta={-0.05} deltaGood="down" />
</Stack>
Banner #
A message across the screen: a connection that expired, an error to act on, a success.
| Prop | Type | Notes |
|---|---|---|
tone required |
'info' | 'success' | 'warning' | 'danger' |
|
text required |
string |
|
title |
string |
|
action |
{ label: string; onPress: () => void } |
One button; label never empty (a11y.action_label_missing) |
onDismiss |
() => void |
Shows a close button; remove the banner in the handler |
{error ? <Banner tone="danger" title="Sunucuya ulaşılamıyor" text={error.message} action={{ label: 'Tekrar dene', onPress: () => items.refetch() }} /> : null}
Image #
An image from the app's own server.
| Prop | Type | Notes |
|---|---|---|
src required |
string |
https: URL ≤ 2048 (http: only to local addresses in development) |
alt required |
string |
What it shows, read by screen readers; "" only for decoration (validate --strict warns a11y.image_alt_missing) |
aspectRatio |
number |
Width / height, 0.1–10; default 16/9 |
fit |
'cover' | 'contain' |
Default cover |
poster |
string |
A still shown while src loads or when it fails; same URL rule |
Origin rule (the host enforces it): src and poster must be on the origin of the manifest
remote.url (or the installation's override) or on a host granted as http:<host>. Any other
image is drawn as a neutral placeholder and logged as a warning; it is not an error. Serve images
from your app server (or a CDN host you list as http:<host>); signed, expiring URLs work.
<Image src={item.thumbnailUrl} alt={item.title} aspectRatio={1} fit="cover" poster={item.posterUrl} />
Avatar #
| Prop | Type | Notes |
|---|---|---|
name required |
string |
The person's or brand's name: read aloud and used for the initials. Never empty (a11y.avatar_name_missing) |
src |
string |
Picture, with the Image origin rule; initials when missing or broken |
size |
'sm' | 'md' | 'lg' |
ProgressBar #
| Prop | Type | Notes |
|---|---|---|
value |
number |
0–1; leave it out for an indeterminate bar (the platform's activity indicator) |
label |
string |
E.g. "Yükleniyor…" |
tone |
'default' | 'success' | 'warning' | 'danger' |
Keep the percentage out of label: the bar already shows value itself.
<ProgressBar value={progress} label="Yükleniyor…" />
<Button label="İptal" variant="ghost" onPress={() => task.cancel()} />
Accessibility checks #
validate --strict and the marketplace lint the rendered tree (lintUiTree, errors.md): a
Button, field (TextField, Select, DatePicker, Switch, MultiSelect), Menu item, action
or Banner action without a label, a Tabs item without a label (a11y.tab_label_missing) and
an Avatar without a name (a11y.avatar_name_missing) are errors; an icon-only Menu and an
Image with an empty alt are warnings.
Exported prop types #
ScreenProps, StackProps, CardProps, TextProps, BadgeProps, EmptyStateProps, ListProps,
ListItem, TableProps, TableColumn, TableRow, ChartProps, ChartSeries, FormProps,
TextFieldProps, SelectProps, SelectOption, DatePickerProps, ButtonProps, MenuProps,
MenuItem, TabsProps, TabItem, SwitchProps, MultiSelectProps, StatProps, BannerProps,
ImageProps, AvatarProps, ProgressBarProps, ValueFormat, Action, BadgeData, Key,
Tone, Spacing, IconName, TextChildren.
Icons #
icon props take an Olan icon name. Unknown names fall back to a generic icon. Names:
mail chat meetings calendar projects marketing contacts files settings inbox all-inboxes archive delete report-spam reply reply-all forward compose edit draw mark-unread mark-read flag folder search attachment send send-arrow add minus close clear check check-double check-circle checkbox more back chevron-right chevron-down chevron-up chevron-up-down list day-view refresh copy paste share document document-text photo photo-library camera camera-flip video-play gif emoji poll mic mic-off video video-off call call-end screen-share speaker waveform play pause stop group account lock block sidebar person-add link record signal approval compare clock warning status-open status-active touch face-id fingerprint qr-scan help info pin list-checks seal-check warning-circle undo favorite bell bell-slash timer sign-out crop rotate sticker download tag snooze print