@baystream/ui-core
v1.0.8
Published
Core React UI components and Syncfusion-based grid helpers used by Baystream applications.
Readme
@baystream/ui-core
Core React UI components and Syncfusion-based grid helpers used by Baystream applications.
Install
npm install @baystream/ui-core react react-domVerify published packages (monorepo)
This repository includes a Vite smoke app, library-testing. By default it is an npm workspace member and resolves @baystream/ui-core / @baystream/ui-theme to local packages/* after npm install at the repo root. You can temporarily switch that app to published semver installs to mimic consumers; see the app README. From the repo root: npm run dev:library-testing, npm run build:library-testing, and npm run typecheck:library-testing. Optional VITE_SYNCFUSION_LICENSE lives in library-testing/.env.
TypeScript and IntelliSense
Install from the npm registry (published tarball includes .js + .d.ts). Import only from the package entry — not from deep paths inside this repo:
import { FlexButton, type FlexButtonProps } from '@baystream/ui-core'VS Code / TypeScript should then offer import completions, Ctrl+Space prop suggestions in JSX, and hover docs sourced from exported prop types (e.g. FlexButtonProps, FlexDataGridProps, FlexNavbarProps).
This package also relies on:
@baystream/ui-hooks@baystream/ui-theme@baystream/ui-utils- Syncfusion grid packages used internally by the components
Main exports
Components
FlexButtonFlexColumnConfigurationProviderFlexDataGridFlexNavbarReduxColumnConfiguratorFlexSyncfusionTheme
Theme loading
loadFlexSyncfusionThemeFLEX_SYNCFUSION_THEME_VARIANTSFLEX_SYNCFUSION_THEME_CSS_STEMFLEX_SYNCFUSION_ICONS_CSS_STEM
Syncfusion data helpers
DataManagerQueryUrlAdaptor
Grid services
ClipboardContextMenuFilterInfiniteScrollPageResizeSearchSelectionSortToolbarVirtualScroll
Quick start
Load a Syncfusion theme once, then render Baystream components.
import { useEffect } from 'react'
import {
FlexButton,
FlexDataGrid,
FlexNavbar,
loadFlexSyncfusionTheme,
} from '@baystream/ui-core'
import { flexSpacing } from '@baystream/ui-theme'
type InvoiceRow = {
id: number
customer: string
amount: number
status: string
}
const rows: InvoiceRow[] = [
{ id: 1001, customer: 'Acme Ltd', amount: 1800, status: 'Paid' },
{ id: 1002, customer: 'Nova Retail', amount: 4200, status: 'Pending' },
]
export function BillingDashboard() {
useEffect(() => {
void loadFlexSyncfusionTheme('bootstrap5')
}, [])
return (
<div style={{ padding: flexSpacing.lg }}>
<FlexNavbar
appName="Baystream Billing"
rightMenus={[
{
id: 'invoices',
label: 'Invoices',
onClick: () => {
/* e.g. navigate('/invoices') */
},
},
{
id: 'customers',
label: 'Customers',
onClick: () => {
/* e.g. navigate('/customers') */
},
},
]}
rightEmail="[email protected]"
rightEmailMenuItems={[
{ label: 'User Management', onClick: () => console.log('users') },
{ label: 'Logout', onClick: () => console.log('logout') },
]}
/>
<div style={{ marginTop: flexSpacing.lg, display: 'flex', gap: flexSpacing.sm }}>
<FlexButton color="primary">Create invoice</FlexButton>
<FlexButton variant="outline" color="secondary">
Export
</FlexButton>
</div>
<div style={{ marginTop: flexSpacing.lg }}>
<FlexDataGrid<InvoiceRow>
dataSource={rows}
columns={[
{ field: 'id', headerText: 'Invoice ID', width: 120, isPrimaryKey: true },
{ field: 'customer', headerText: 'Customer', width: 220 },
{ field: 'amount', headerText: 'Amount', width: 140 },
{ field: 'status', headerText: 'Status', width: 140 },
]}
/>
</div>
</div>
)
}Theme setup
Syncfusion styles are global. Load one theme near app startup before rendering grid-based components.
Option 1: runtime loader
import { useEffect } from 'react'
import { loadFlexSyncfusionTheme } from '@baystream/ui-core'
export function AppThemeBoot() {
useEffect(() => {
void loadFlexSyncfusionTheme('bootstrap5')
}, [])
return null
}Option 2: direct theme import
import '@baystream/ui-core/themes/bootstrap5'Available theme variants:
materialmaterialDarkmaterial3material3Darkbootstrap5bootstrap5Darkfluent2fluentfluentDarkfabricfabricDarktailwind3
FlexButton
Button component with semantic colors, variants, sizes, icons, and loading state.
Props
| Prop | Type | What it does |
| --- | --- | --- |
| variant | contained \| outline \| ghost | Selects the visual button style. |
| color | semantic color name | Chooses the semantic action color. |
| size | sm \| md \| lg | Controls padding and font scale. |
| startIcon | ReactNode | Renders content before the label. |
| endIcon | ReactNode | Renders content after the label. |
| loading | boolean | Disables the button and shows a loading indicator. |
| loadingIndicator | ReactNode | Replaces the default loading marker. |
| loadingLabel | ReactNode | Replaces the visible label while loading. |
| unstyled | boolean | Turns off built-in inline styles. |
| backgroundColor | string | Per-instance background override. |
| outlineColor | string | Per-instance border color override. |
| borderRadius | CSS radius value | Per-instance border radius override. |
| customStyles | CSSProperties | Merges a custom style object on top. |
variant:contained | outline | ghostcolor:primary | secondary | success | warning | info | light | dark | error | dangersize:sm | md | lgstartIconendIconloadingloadingIndicatorloadingLabelunstyledbackgroundColoroutlineColorborderRadiuscustomStyles
Example: action bar
import { FlexButton } from '@baystream/ui-core'
export function ActionBar() {
return (
<div style={{ display: 'flex', gap: 12 }}>
<FlexButton color="primary" variant="contained">
Save
</FlexButton>
<FlexButton color="secondary" variant="outline">
Cancel
</FlexButton>
<FlexButton color="danger" variant="ghost">
Delete
</FlexButton>
</div>
)
}Example: loading state with icons
import { useState } from 'react'
import { FlexButton } from '@baystream/ui-core'
export function UploadButton() {
const [loading, setLoading] = useState(false)
async function handleUpload() {
setLoading(true)
try {
await new Promise((resolve) => setTimeout(resolve, 1500))
} finally {
setLoading(false)
}
}
return (
<FlexButton
color="primary"
startIcon="^"
endIcon=">"
loading={loading}
loadingLabel="Uploading..."
loadingIndicator="o"
onClick={handleUpload}
>
Upload file
</FlexButton>
)
}FlexDataGrid
Declarative wrapper around Syncfusion Grid with Baystream defaults for sorting, menu filtering, toolbar search, selection, resize, reorder, clipboard, and context menus.
Props
| Prop | Type | What it does |
| --- | --- | --- |
| columns | column definition array | Declares visible grid columns and optional checkbox selection column. |
| dataSource | row array or DataManager | Supports local arrays or remote/adaptor-backed data. |
| allowSorting | boolean | Enables header sorting. |
| allowFiltering | boolean | Enables column filter menus. |
| allowSearching | boolean | Enables toolbar search behavior. |
| allowSelection | boolean | Enables row selection behavior. |
| allowColumnVisibilityToggle | boolean | Adds the Baystream header hide-column behavior. |
| builtInColumnChooser | boolean | Adds Syncfusion's column chooser when visibility toggle is enabled. |
| rowHeight | number | Controls row density in pixels. |
| fontSize | CSS font size | Scales grid text size. |
| headerFontSize | CSS font size | Column headers only; body cells keep following fontSize. |
| headerHeight | CSS height | Header and filter-bar row cell height (applied via CSS). Suggested token: flexLayout.dataGridHeaderHeight from @baystream/ui-theme. |
| injectServices | Syncfusion service array | Adds services such as Page, VirtualScroll, or InfiniteScroll. |
| toolbar | toolbar item array | Controls Syncfusion toolbar actions such as Search. |
| height | number or CSS size | Sets the rendered grid height. |
| remoteLoadingIndicators | boolean | When true, skeleton until first dataBound, footer bar on later infiniteScroll fetches. Hides Syncfusion’s default grid spinner so loading UI is not duplicated. |
| remoteLoadingSkeletonRows | number | Skeleton row count for the initial overlay (default 8). |
| remoteLoadingMoreLabel | string | Footer text while loading the next block (default "Loading more…"). |
columnsdataSourceallowSortingallowFilteringallowSearchingallowSelectionallowColumnVisibilityTogglebuiltInColumnChooserrowHeightfontSizeheaderFontSizeheaderHeightinjectServices
Example: local data grid
import { FlexDataGrid } from '@baystream/ui-core'
import { flexLayout } from '@baystream/ui-theme'
type ProductRow = {
id: string
name: string
category: string
price: number
isSelected?: boolean
}
const rows: ProductRow[] = [
{ id: 'P-100', name: 'Keyboard', category: 'Hardware', price: 49 },
{ id: 'P-101', name: 'Mouse', category: 'Hardware', price: 25 },
{ id: 'P-102', name: 'Desk License', category: 'Software', price: 99 },
]
export function ProductGrid() {
return (
<FlexDataGrid<ProductRow>
dataSource={rows}
columns={[
{ type: 'checkbox', width: 56 },
{ field: 'id', headerText: 'ID', width: 110, isPrimaryKey: true },
{ field: 'name', headerText: 'Name', width: 220 },
{ field: 'category', headerText: 'Category', width: 160 },
{ field: 'price', headerText: 'Price', width: 120 },
]}
allowSelection
allowSearching
allowSorting
allowFiltering
rowHeight={36}
headerHeight={flexLayout.dataGridHeaderHeight}
fontSize={14}
/>
)
}Example: column chooser and hide/show
import { FlexDataGrid } from '@baystream/ui-core'
type CustomerRow = {
id: number
name: string
email: string
region: string
}
const customers: CustomerRow[] = [
{ id: 1, name: 'Asha', email: '[email protected]', region: 'North' },
{ id: 2, name: 'Ravi', email: '[email protected]', region: 'South' },
]
export function CustomerGrid() {
return (
<FlexDataGrid<CustomerRow>
dataSource={customers}
columns={[
{ field: 'id', headerText: 'ID', isPrimaryKey: true, showInColumnChooser: false },
{ field: 'name', headerText: 'Name' },
{ field: 'email', headerText: 'Email', visible: false },
{ field: 'region', headerText: 'Region' },
]}
allowColumnVisibilityToggle
builtInColumnChooser
toolbar={['Search']}
/>
)
}Example: remote data source
import { DataManager, UrlAdaptor, FlexDataGrid } from '@baystream/ui-core'
type ApiRow = {
id: number
name: string
}
const dataSource = new DataManager({
url: '/api/users',
adaptor: new UrlAdaptor(),
})
export function RemoteUsersGrid() {
return (
<FlexDataGrid<ApiRow>
dataSource={dataSource}
columns={[
{ field: 'id', headerText: 'ID', isPrimaryKey: true, width: 90 },
{ field: 'name', headerText: 'Name', width: 220 },
]}
/>
)
}Remote grids: adaptors, GET, and server-side sort
The snippet above assumes an API that matches Syncfusion’s default POST + JSON body UrlAdaptor behavior. Real REST endpoints often differ:
| Topic | What to know |
| --- | --- |
| GET + query string | Set adaptor.options.requestType = 'get' and override convertToQueryString so skip / take (and filters) become your query params (e.g. skip, limit). |
| Response shape | The grid expects result + count. Map your JSON in processResponse. Base UrlAdaptor.processResponse may call JSON.parse(request.data) — inappropriate when the response body is in the fetch result, not request.data — so many apps subclass and map without calling that path. |
| Server-side sorting | Column sorts issue new remote requests. Syncfusion accumulates sort descriptors in req[adaptor.options.sortBy] (default key sorted) as { name, direction }[]. Your adaptor must translate that into your API’s sort contract (sortBy / order, $orderby, sort=-field, POST fields, etc.). Sorting only “in the browser” happens with a local array dataSource or if sort params are never sent. |
| Full demonstration | In this monorepo, library-testing is an end-to-end sample: remote DataManager, GET DummyJsonProductsAdaptor (pagination + server-side sort mapping), infinite scroll, dynamic columns from dynamicProductColumns.ts, Redux column configuration, and optional remoteLoadingIndicators (skeleton + batch loader). Run npm run dev:library-testing from the repo root. |
Example: virtualization
import { FlexDataGrid, VirtualScroll } from '@baystream/ui-core'
const rows = Array.from({ length: 2000 }, (_, index) => ({
id: index + 1,
name: `Item ${index + 1}`,
}))
export function VirtualizedGrid() {
return (
<FlexDataGrid<{ id: number; name: string }>
dataSource={rows}
columns={[
{ field: 'id', headerText: 'ID', isPrimaryKey: true, width: 90 },
{ field: 'name', headerText: 'Name', width: 220 },
]}
height={420}
enableVirtualization
injectServices={[VirtualScroll]}
/>
)
}FlexNavbar
Top navigation bar with brand area, optional right-side actions (onClick only), and optional user email dropdown.
Props
| Prop | Type | What it does |
| --- | --- | --- |
| appName | string | Primary brand text shown in the navbar. |
| variant | withRightMenus \| withoutRightMenus | Toggles whether right-side menus are expected. |
| sticky | boolean | Keeps the navbar pinned to the top when true. |
| logo | ReactNode | Replaces the default logo image with custom content. |
| logoSrc | string | Replaces the default logo image source. |
| logoAlt | string | Sets the logo image alt text. |
| logoTextFontSize | CSS font size | Overrides brand text size. |
| onBrandClick | () => void | Handles brand click action. |
| rightMenus | { id, label, onClick }[] | Renders right-side actions as buttons; host uses onClick for routing or anything else (no href). |
| rightEmail | string | Shows the signed-in email text or menu trigger. |
| rightEmailMenuItems | { label \| text, onClick, ... }[] | Account dropdown items; each row is a button and uses onClick. |
| rightSlot | ReactNode | Adds custom content at the far right. |
| appBarProps | app bar props | Passes through lower-level app bar customization. |
appNamevariantstickylogologoSrclogoAltlogoTextFontSizeonBrandClickrightMenusrightEmailrightEmailMenuItemsrightSlot
Example: standard app header
import { FlexNavbar } from '@baystream/ui-core'
export function AppHeader() {
return (
<FlexNavbar
appName="Baystream Admin"
rightMenus={[
{
id: 'dashboard',
label: 'Dashboard',
onClick: () => {
/* navigate('/dashboard') or your router */
},
},
{
id: 'reports',
label: 'Reports',
onClick: () => {
/* navigate('/reports') */
},
},
]}
rightEmail="[email protected]"
rightEmailMenuItems={[
{ label: 'User Management', onClick: () => console.log('users') },
{ label: 'Logout', onClick: () => console.log('logout') },
]}
/>
)
}Example: custom brand and no right links
import { FlexNavbar } from '@baystream/ui-core'
export function FormsHeader() {
return (
<FlexNavbar
appName="FlexForms"
variant="withoutRightMenus"
logo={<span aria-hidden>BF</span>}
rightEmail="[email protected]"
rightEmailMenuItems={[
{ label: 'Logout', onClick: () => console.log('logout') },
]}
/>
)
}Data helpers
DataManager, Query, and UrlAdaptor are re-exported so consumers can build remote grid integrations without importing directly from @syncfusion/ej2-data.
import { DataManager, Query, UrlAdaptor } from '@baystream/ui-core'
const manager = new DataManager({
url: '/api/orders',
adaptor: new UrlAdaptor(),
})
const query = new Query().take(25)
void manager
void queryColumn configuration
Use FlexColumnConfigurationProvider, useFlexColumnConfiguration, ReduxColumnConfigurator, and useSyncfusionGridColumnVisibility when you want persisted column visibility/order with FlexDataGrid and no Syncfusion Column Chooser toolbar.
The default path is plug-and-play: ui-core owns a small Redux store and persists it to localStorage.
import {
FlexColumnConfigurationProvider,
FlexDataGrid,
ReduxColumnConfigurator,
useFlexColumnConfiguration,
useSyncfusionGridColumnVisibility,
type FlexGridHandle,
} from '@baystream/ui-core'
import { useRef } from 'react'
function OrdersGridInner() {
const gridRef = useRef<FlexGridHandle | null>(null)
const columnConfiguration = useFlexColumnConfiguration({
columns: ORDER_COLUMNS,
storageKey: 'orders-grid-columns',
defaultVisibleFields: ['id', 'customer', 'amount'],
})
useSyncfusionGridColumnVisibility(
gridRef,
columnConfiguration.columnConfigs,
true,
)
return (
<>
<ReduxColumnConfigurator
columnConfigs={columnConfiguration.columnConfigs}
defaultColumnConfigs={columnConfiguration.defaultColumnConfigs}
onApply={columnConfiguration.applyColumnConfigs}
/>
<FlexDataGrid
ref={gridRef}
columns={columnConfiguration.mergedColumns}
dataSource={orders}
allowColumnVisibilityToggle
allowReordering
columnVisibilityChanged={columnConfiguration.columnVisibilityChanged}
actionComplete={columnConfiguration.actionComplete}
/>
</>
)
}
export function OrdersGrid() {
return (
<FlexColumnConfigurationProvider>
<OrdersGridInner />
</FlexColumnConfigurationProvider>
)
}Use storage="memory" for non-persistent demos/tests, or localStorageKey when you want to namespace the browser storage key.
Advanced: bind to an app Redux store
If a host app wants this state in its own Redux tree, mount columnConfigurationReducer under a stable key and bind the hook with createUseColumnConfiguration:
import { combineReducers } from '@reduxjs/toolkit'
import { persistReducer } from 'redux-persist'
import storage from 'redux-persist/lib/storage'
import { columnConfigurationReducer } from '@baystream/ui-core'
const persistedReducerInner = combineReducers({
columnConfiguration: columnConfigurationReducer,
})
export const persistedReducer = persistReducer(
{ key: 'persisted', storage, whitelist: ['columnConfiguration'] },
persistedReducerInner,
)Bind the column hook to your store
import { createUseColumnConfiguration } from '@baystream/ui-core'
import { useDispatch, useSelector } from 'react-redux'
import type { AppDispatch, RootState } from './redux/store'
export const useAppDispatch = useDispatch.withTypes<AppDispatch>()
export const useAppSelector = useSelector.withTypes<RootState>()
/** True once redux-persist has rehydrated, or always `true` if you do not use persist */
export function selectPersistedRehydrated(state: RootState): boolean {
const pe = state.persisted._persist
return pe == null ? true : pe.rehydrated === true
}
export const useColumnConfiguration = createUseColumnConfiguration<
RootState,
AppDispatch
>({
useAppDispatch,
useAppSelector,
selectColumnConfigurationState: (s) => s.persisted.columnConfiguration,
selectPersistedRehydrated,
})Then use your bound useColumnConfiguration, ReduxColumnConfigurator, and useSyncfusionGridColumnVisibility the same way the plug-and-play example uses useFlexColumnConfiguration.
Notes
- Syncfusion theme CSS is global. In production, pick one theme per page load.
- For large datasets, combine
enableVirtualizationwithVirtualScroll. - For internal Baystream apps, prefer these wrappers over importing Syncfusion grid pieces directly unless you need behavior not exposed here.
