The library started as the components/ui folders of Arbol’s three dashboards. Most components keep the
apps’ names and props, so moving a screen is usually changing an import from ~/components/ui/… to
@arbol-ai/ui. This page lists what differs.
Setup, once per app
bun add @arbol-ai/ui(React Aria, React and Tailwind are already there).- In the entry stylesheet, after Tailwind:
@import '@arbol-ai/ui/theme.css';. Keep the app’s ownfonts.css(the theme names Arbol Sans; it never ships font files). The theme does not ship the apps’ global* { min-width: 0 }or the unlayeredh1, h2 { font-family: Arbolito }rule: keep them in the app while its own screens depend on them. - Render
RouterUIProviderfrom@arbol-ai/ui/routerat the root (it isUIProviderwired to TanStack Router:navigate({ href }), and base paths only for app paths, sotel:andwa.melinks stay intact), with the locale:- Raíz and Account (Paraglide):
locale={getLocale() === 'es' ? 'es-CO' : 'en-US'}. - Agenda (Spanish only):
locale="es-CO". Agenda had noI18nProvider, so React Aria’s own strings (search clear button, drop zone…) followed the browser’s language; this fixes it.
- Raíz and Account (Paraglide):
- Mount
<Toaster />once, insideUIProvider, to replace Raíz’s sonnerToaster.
Renamed props and changed defaults
| Component | Before (app) | Now |
|---|---|---|
Badge |
variant="success", "destructive-soft"… |
tone (neutral, brand, success, warning, danger, info) and variant (soft, outline); variant="outline" still works. JSDoc maps every old name. |
Tooltip (Raíz) |
Tooltip + TooltipContent |
TooltipTrigger + Tooltip (React Aria’s names). |
SaveBar |
dirty, busy, label |
isDirty, isPending, saveLabel. |
Notice |
default tone success |
default tone info: pass tone="success" where a bare <Notice> meant success. |
ErrorState |
heading h1 |
heading h2 by default; pass level={1} when it is the page. |
Select (Raíz) |
value: string, onValueChange |
React Aria’s value / onChange; label, description and error built in. |
PhoneField (Raíz PhoneInput) |
onValueChange, disabled, invalid, emptyLabel |
onChange, isDisabled, isInvalid; the empty text is a library string. |
CursorPagination (Raíz ListPagination) |
onPrev |
onPrevious. |
FilterToggle (Agenda InactiveFilter) |
selected |
isSelected. |
UnsavedChanges (Agenda) |
imported TanStack Router | <UnsavedChangesDialog {...useUnsavedChangesBlocker(isDirty)} /> from @arbol-ai/ui/router. |
RowMenu |
navigated with useNavigate |
uses href, routed by UIProvider navigate. |
Overlay |
onClose after a 200 ms timer |
same API; onClose fires after the exit animation ends. Open it from a button with OverlayTrigger, not React Aria’s DialogTrigger. |
ColorSwatchPicker (Agenda ColorPicker) |
oklch palette | pass the palette as hex (#rrggbb), rgb() or hsl(). |
Sheet (Raíz) |
non-modal | always modal; the assistant side panel stays app layout. |
ConfirmationDialog (Raíz, two of them) |
labels object, fixed DOM ids |
one ConfirmDialog with plain props and an optional type-to-confirm value. |
| Toasts (Raíz, sonner) | toast.error(message) |
the same call shape from @arbol-ai/ui. |
Fixed on the way
- White text on the old destructive red (3.8:1) and on the 90%-opacity primary hover failed WCAG AA; both now pass.
- Account’s
SearchInputignored a controlled value on the first render; Agenda’s could run a stale search. - Account’s
SettingToggleCardnested a<label>in a<label>; Raíz’sEditableTitlerendered<button><h1>. - Raíz’s breadcrumbs announced “breadcrumb” in English; DataTable rows had no keyboard path; nav landmarks had no names.
- Agenda’s pick list built plurals by concatenation (“1 seleccionado/s”).