@econneq/headless-crud
v3.0.2
Published
Headless, framework-agnostic CRUD engine — renderMode, tableVariant, embedded filter bar, REST+GraphQL, dark/light mode, full TypeScript.
Maintainers
Readme
@econneq/headless-crud v4
Headless, framework-agnostic CRUD engine for React, Next.js, React Native and any JS project. Zero external UI dependencies — batteries-included teal design system with full dark/light mode.
What's new in v4
| Feature | Details |
|---|---|
| renderMode | 'full' · 'table' · 'form' · 'action' |
| tableVariant | 'enterprise' (A) · 'card' (B) |
| Embedded filter bar | Accordion inside the table header — never detached |
| FormOnly | Inline form without any button click |
| ActionButton | Button-only render — reveals form/table on click |
| REST adapter v2 | Entity-routing, body transforms, idPlacement |
| Headless core | createCrudCore() — pure TS, no React, use anywhere |
| Theme overrides | injectThemeOverrides() + buildThemeVars() for RN |
| colorMode | 'light' · 'dark' · 'auto' (follows system) |
Install
npm install @econneq/headless-crud
# or
yarn add @econneq/headless-crudQuick start
import { CrudEngine, CrudProvider } from '@econneq/headless-crud';
// Wrap your app once
<CrudProvider adapters={{ toast, useMutation }}>
<App />
</CrudProvider>
// Use anywhere
<CrudEngine config={config} data={data} />Render modes
Scenario 1 — Table only (no add button)
<CrudEngine
config={config}
data={data}
renderMode="table"
tableVariant="card" // 'enterprise' or 'card'
filter={{
show: true, // attach filter bar to table header
expanded: true, // open by default
mode: 'client', // 'client' | 'server'
fields: [
{ key: 'name', label: 'Name', type: 'text' },
{ key: 'status', label: 'Status', type: 'select', options: [...] },
],
columns: { mobile: 1, tablet: 2, desktop: 3 },
}}
/>Scenario 2 — Form only (inline, no button required)
<CrudEngine
config={config}
data={[]}
renderMode="form"
formOnly={{
autoOpen: true, // default true — mounts immediately
defaultMode: 'create', // 'create' | 'edit'
initialValues: { name: '' },
showFooter: true,
submitLabel: 'Save Patient',
}}
onMutationSuccess={() => router.push('/patients')}
/>Scenario 3 — Action button only
<CrudEngine
config={config}
data={data}
renderMode="action"
actionButton={{
label: '+ New / Search Patient',
variant: 'primary',
size: 'md',
shows: 'form', // 'form' | 'table' | 'both'
position: 'slide-over', // 'modal' | 'slide-over' | 'inline'
}}
/>Table variants
Variant A — Enterprise (default)
Teal header, export toolbar, column visibility panel.
<CrudEngine config={config} data={data} tableVariant="enterprise" />Variant B — Card
PageHeader-style title, card container, inline search. Matches the patient table example exactly.
<CrudEngine
config={config}
data={data}
tableVariant="card"
cardTable={{
title: "Today's Patients",
subtitle: `${total} visits on ${TODAY}`,
searchPlaceholder: 'Filter by name, phone…',
showFooter: true,
backLabel: '← Back',
onBack: () => router.back(),
}}
/>Filter bar
The filter bar is embedded inside the table card — not a separate component.
<CrudEngine
config={config}
data={data}
filter={{
show: true,
expanded: true, // accordion starts open
label: 'Search Filters',
mode: 'server',
apiType: 'rest',
rest: {
url: '/api/patients/search',
method: 'GET',
paramType: 'query',
},
behavior: { mode: 'debounce', debounceMs: 600 },
useUrlParams: true,
urlParamPrefix: 'f_',
fields: [
{ key: 'name', label: 'Name', type: 'text' },
{ key: 'dob', label: 'DOB', type: 'date' },
{ key: 'sex', label: 'Sex', type: 'select', options: [
{ value: 'M', label: 'Male' },
{ value: 'F', label: 'Female' },
]},
{ key: 'phone', label: 'Phone', type: 'text' },
],
columns: { mobile: 1, tablet: 2, desktop: 4 },
}}
/>Server filter — GraphQL
filter={{
show: true,
mode: 'server',
apiType: 'graphql',
onServerFilter: (filters) => {
refetch({ where: buildWhereClause(filters) });
},
fields: [...],
}}Dark / light mode
// Follow system preference (default)
<CrudEngine colorMode="auto" ... />
// Force dark
<CrudEngine colorMode="dark" ... />
// Force light
<CrudEngine colorMode="light" ... />
// Or via HTML attribute on your root element
document.documentElement.setAttribute('data-theme', 'dark');Theme overrides
// Per-component override
<CrudEngine
themeOverrides={{
brand: '#7c3aed',
font: 'Inter, sans-serif',
tableHeaderBg: '#4c1d95',
}}
...
/>
// Global override (call once at app startup)
import { injectThemeOverrides } from '@econneq/headless-crud';
injectThemeOverrides({ brand: '#7c3aed', radius: '8px' });Framework agnostic — headless core
The createCrudCore() factory is pure TypeScript with zero React imports.
Use it in Vue, Svelte, React Native, or vanilla JS:
import { createCrudCore } from '@econneq/headless-crud/core';
const engine = createCrudCore({ fields, defaults, validate });
// Subscribe to state changes
const unsubscribe = engine.subscribe((state) => {
myStore.set(state); // Svelte writable / Vue reactive
});
engine.openCreate();
engine.handleChange('name', 'Alice');
const errors = engine.doValidate(engine.state.formValues);
const payload = engine.buildPayload(engine.state.formValues);React hook adapter (already built-in)
import { useCrudForm } from '@econneq/headless-crud';
const form = useCrudForm({ config, externalData });React Native usage
Theme tokens as JS object (no CSS):
import { buildThemeVars } from '@econneq/headless-crud';
const theme = buildThemeVars(isDark);
// theme.brand, theme.surface, theme.tableHeaderBg, etc.REST adapter
import { makeRestAdapter } from '@econneq/headless-crud/adapters/rest';
const adapters = makeRestAdapter({
toast,
baseUrl: '/api',
entity: 'patients',
getToken: () => localStorage.getItem('token'),
idPlacement: 'path', // PUT /api/patients/:id
methods: { create: 'POST', update: 'PUT', delete: 'DELETE' },
transformBody: (vars, action) => ({
...vars,
_action: action,
}),
});GraphQL (Apollo) adapter
import { makeApolloAdapter } from '@econneq/headless-crud/adapters/apollo';
const adapters = makeApolloAdapter({ toast, useMutation, decodeId });TanStack Query adapter
import { makeTanstackAdapter } from '@econneq/headless-crud/adapters/tanstack-query';
const { data, isLoading, isFetching, dataUpdatedAt } = useQuery(...);
<CrudEngine
extraTableProps={{ isFetching, dataUpdatedAt }}
onMutationSuccess={() => queryClient.invalidateQueries(...)}
...
/>Full config reference
interface CrudEngineProps {
config: CrudConfig;
data: T[];
loading?: boolean;
renderMode?: 'full' | 'table' | 'form' | 'action'; // default: 'full'
tableVariant?: 'enterprise' | 'card'; // default: 'enterprise'
filter?: FilterBarConfig;
formOnly?: FormOnlyConfig;
actionButton?: ActionButtonConfig;
cardTable?: CardTableConfig;
colorMode?: 'light' | 'dark' | 'auto'; // default: 'auto'
themeOverrides?: ThemeOverrides;
exportOptions?: TableExportOptions;
viewLink?: ViewLinkConfig;
tableControls?: TableDisplayControls;
fixedVariables?: Record<string, any>;
externalData?: Record<string, any[]>;
onRefresh?: () => void;
onMutationSuccess?: () => void;
className?: string;
extraTableProps?: Record<string, any>;
}Changelog
v4.0.0
renderMode:table|form|action|fulltableVariant:enterprise(A) |card(B)FilterBarembedded in table header with accordion, client + server + REST + GraphQLFormOnlycomponent for inline formsActionButtoncomponent for button-only rendercreateCrudCore()framework-agnostic headless engineinjectThemeOverrides()runtime token injectionbuildThemeVars()for React Native / non-DOMcolorModeprop:light|dark|auto- REST adapter v2 with entity routing,
idPlacement, body transforms - All filter fields responsive with configurable grid
- URL param sync for filters
