nexbase-ui
v1.0.0
Published
Lightweight React component library — buttons, inputs, modals, drawers, toasts, tabs and a flag-driven DataGrid with a built-in server mode for paginated, filterable REST APIs. Themeable via CSS variables, dark mode built in, zero runtime dependencies.
Maintainers
Readme
nexbase-ui
Lightweight React component library — theming, dark mode, and a flag-driven DataGrid with a built-in server mode for paginated, filterable REST APIs. Zero runtime dependencies (React peer only).
Install
npm install nexbase-uiimport { ThemeProvider, ToastProvider, Button } from 'nexbase-ui';
import 'nexbase-ui/styles.css';
export const App = () => (
<ThemeProvider defaultMode="system"> {/* light | dark | system */}
<ToastProvider>
<Button>Hello</Button>
</ToastProvider>
</ThemeProvider>
);Components
| Group | Exports |
|---|---|
| Theme | ThemeProvider, useTheme |
| Primitives | Button, IconButton, Badge, Chip, Avatar, Spinner, Divider, Tooltip |
| Inputs | Input, Textarea, Select, Checkbox, Radio, Switch, FormField, Combobox, DatePicker, DateRangePicker |
| Feedback | Alert, Skeleton, EmptyState, ToastProvider, useToast |
| Overlays | Modal, Drawer, Dropdown |
| Navigation | Card, Tabs, Pagination |
| Data | DataGrid, useDataGrid (headless), resolveUrl, buildQueryString, KanbanBoard |
| Charts | Sparkline, LineChart, BarChart, PieChart |
| Layout | AppShell, Sidebar, Header, Content, MetricCard |
Everything is a named export → tree-shakable. Modal/Drawer have focus-trap + Esc; toasts via useToast().success('Saved').
DataGrid
Server mode — point at any REST endpoint implementing the convention below, zero glue:
<DataGrid
endpoint="/api/leads"
auth={{ token: () => localStorage.getItem('jwt') }}
defaultSort={['created_at', 'desc']}
columns={[
{ key: 'name', label: 'Name', sortable: true },
{ key: 'status', label: 'Status', sortable: true, filterable: true, editable: true,
values: ['new', 'won', 'lost'], render: (r) => <Badge tone="info">{r.status}</Badge> },
{ key: 'created_at', label: 'Created', sortable: true }
]}
flags={{
search: true,
pagination: { sizes: [10, 25, 50], default: 10 },
selection: true,
export: 'csv,xlsx',
inlineEdit: true, // double-click a cell, Enter commits, Esc cancels
keyboardNav: true // j/k move, Enter opens, x selects
}}
onRowClick={(row) => openDetails(row)}
bulkActions={[{ label: 'Archive', onClick: (ids, done) => archive(ids).then(done) }]}
/>Client mode — small datasets, no server:
<DataGrid rows={localRows} columns={cols} flags={{ search: true }} />Server-mode contract
All list/search/sort/filter/pagination state is sent as query-string params — this mirrors nexbase's REST contract exactly, so pointing endpoint at a nexbase resource works with zero glue:
- List:
GET <list url>?page=&page_size=&sort=&order=&search=&filter[col]=v&filter[col][op]=v, expecting{ records, total }. - Export:
GET <export url>?...&format=csv|xlsx, expecting a file response (blob). - Inline edit:
PATCH <edit url>/<id>with body{ [key]: value }by default, or youronEdit(id, patch). - Errors:
{ error: { code, message } }on non-2xx.
Filter operators: eq (default) neq like in gte lte gt lt between, e.g.
filter[amount][between]=100,500, filter[status][in]=new,won. buildQueryString(query) (exported)
builds this query string from { page, page_size, sort, order, search, filters, format }, where
filters[col] is either a plain value (op eq) or { op, value }.
Pointing at your own backend
By default the list/export/edit URLs are derived from endpoint (<endpoint>?..., <endpoint>/export?..., <endpoint>/<id>). To work with a backend that uses different paths without changing any backend code:
url— a custom base URL used instead ofendpointfor the same default suffixing.urlBuilder— a function({ action, endpoint, url, id, query }) => stringcalled for every request (actionis'list' | 'export' | 'edit' | 'delete' | 'create'). Takes full precedence — use this when your routes don't follow the/export//:idpattern at all. Forlist/export, build the query string yourself with the exportedbuildQueryString(query)if you need it.fetcher— a function({ url, method, headers }) => Promise<json>to fully replace the defaultfetch-based request (e.g. to talk to GraphQL, add custom headers, or reshape responses).
<DataGrid
endpoint="/api/leads"
urlBuilder={({ action, id, query }) =>
action === 'export' ? `/api/leads/export-all${buildQueryString(query)}`
: action === 'edit' ? `/api/leads/${id}/update`
: `/api/leads/search${buildQueryString(query)}`}
/>Error handling
List, export, and inline-edit failures all surface as an <Alert tone="danger"> banner at the top of the grid (dismissible for export/edit failures).
Theming
Override any token — that's the whole theming API:
<ThemeProvider tokens={{ '--ui-accent': '#0ea5e9', '--ui-radius': '4px' }}>Dark mode automatic in system mode; useTheme().setMode('dark') to control.
Module format
Published as pre-compiled ESM and CommonJS (built from src/ via tsup):
import { Button } from 'nexbase-ui'→dist/index.js(ESM).require('nexbase-ui')→dist/index.cjs(CJS) — no moreERR_REQUIRE_ESM.import 'nexbase-ui/styles.css'→dist/styles.css.react/react-domare external peer dependencies — never bundled, resolved from the host app.
JSX is compiled away at publish time, so consumers don't need a JSX-aware toolchain just to
resolve this package — plain node -e "require('nexbase-ui')" works. src/ (plain .jsx/.js) is
the source of truth; run npm run build to produce dist/ (also runs automatically via
prepublishOnly before npm publish).
License
MIT
