@econneq/headless-crud
v3.0.1
Published
E-CONNEQ branded headless CRUD engine with TanStack Query adapter. Zero external UI deps — batteries-included teal theme.
Downloads
1,926
Maintainers
Readme
@econneq/headless-crud — v2.0.0
E-CONNEQ Technologies — Headless, configuration-driven CRUD engine with a built-in brand theme and TanStack Query adapter. Zero external UI dependencies.
What's new in v2
| Feature | Details |
|---|---|
| E-CONNEQ Brand Theme | Full teal design system derived from the logo — injected automatically, no CSS imports needed |
| TanStack Query Adapter | First-class useMutation + useQuery integration with stale/fetching indicators |
| Skeleton loaders | Tables shimmer while loading instead of showing a spinner row |
| Action-semantic buttons | 🟢 Create · 🔵 Edit · 🔴 Delete · 🩵 Default (teal) |
| Dark mode | Auto via prefers-color-scheme or html[data-theme="dark"] |
| DM Sans font | Loaded via Google Fonts in the injected CSS |
| Built-in toast | No external lib needed — makeEconneqToast() works out of the box |
Installation
npm install @econneq/headless-crud
# peer deps
npm install react react-dom
# optional (for TanStack Query adapter)
npm install @tanstack/react-queryQuick start — REST + TanStack Query
// app/layout.tsx (or _app.tsx)
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { useMutation } from '@tanstack/react-query';
import { QueryCrudProvider } from '@econneq/headless-crud/adapters/tanstack-query';
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 30_000, // 30 s — adjust per entity
gcTime: 300_000, // 5 min
},
},
});
export default function RootLayout({ children }) {
return (
<QueryClientProvider client={queryClient}>
{/* QueryCrudProvider wires toast + useMutation automatically */}
<QueryCrudProvider useMutation={useMutation}>
{children}
</QueryCrudProvider>
</QueryClientProvider>
);
}// app/users/page.tsx
'use client';
import { useQuery } from '@tanstack/react-query';
import { CrudEngine } from '@econneq/headless-crud';
import { makeRestMutation, queryKeys } from '@econneq/headless-crud/adapters/tanstack-query';
import { injectEconneqTheme } from '@econneq/headless-crud/theme';
import type { CrudConfig } from '@econneq/headless-crud';
// 1 — inject theme once (idempotent)
injectEconneqTheme();
// 2 — REST mutation function
const userMutation = makeRestMutation('/api/users');
// 3 — entity config
const config: CrudConfig<User> = {
entity: 'User',
mutation: userMutation,
formMode: 'slide-over',
fields: [
{ key: 'name', label: 'Full Name', type: 'text', validation: { required: true } },
{ key: 'email', label: 'Email', type: 'email', validation: { required: true, email: true } },
{ key: 'role', label: 'Role', type: 'select',
options: ['Admin', 'Editor', 'Viewer'] },
{ key: 'active', label: 'Active', type: 'toggle' },
],
};
export default function UsersPage() {
const keys = queryKeys('users');
const {
data,
isLoading,
isFetching,
dataUpdatedAt,
refetch,
} = useQuery({
queryKey: keys.list(),
queryFn: () => fetch('/api/users').then(r => r.json()),
});
return (
<CrudEngine
config={config}
data={data ?? []}
loading={isLoading}
onRefresh={refetch}
// Pass TanStack Query extras to the table
// (uses DefaultTable internally — these props are forwarded automatically
// when you pass them as extraTableProps — see below)
/>
);
}TanStack Query — forwarding isFetching & dataUpdatedAt
The built-in DefaultTable accepts two TanStack-specific props:
| Prop | Type | Effect |
|---|---|---|
| isFetching | boolean | Shows a "Refreshing…" spinner in the toolbar (separate from the skeleton loader) |
| dataUpdatedAt | number | Shows "Updated Xs ago" in the toolbar; auto-refreshes every 30 s |
Pass them via CrudEngine's extraTableProps (available from v2):
<CrudEngine
config={config}
data={data ?? []}
loading={isLoading}
onRefresh={refetch}
extraTableProps={{ isFetching, dataUpdatedAt }}
/>Or if you use DefaultTable directly:
<DefaultTable
columns={columns}
data={data}
isFetching={isFetching}
dataUpdatedAt={dataUpdatedAt}
onAdd={openCreate}
/>Apollo / GraphQL
import { useMutation } from '@apollo/client';
import { CrudProvider } from '@econneq/headless-crud';
import { makeEconneqToast } from '@econneq/headless-crud/adapters/tanstack-query';
const apolloAdapter = (mutation, opts) => {
const [mutateFn, { loading }] = useMutation(mutation, opts);
const wrapped = async ({ variables }) => {
try {
const { data, errors } = await mutateFn({ variables });
return { data, errors };
} catch (err) {
return { errors: [{ message: err.message }] };
}
};
return [wrapped, { loading }];
};
<CrudProvider adapters={{ useMutation: apolloAdapter, toast: makeEconneqToast() }}>
<App />
</CrudProvider>Theme reference
Color tokens (CSS custom properties)
/* Brand */
--eq-brand /* #3a8f8f — logo teal */
--eq-brand-dark /* #2e7474 */
--eq-brand-light /* #5ec4c4 */
--eq-brand-muted /* #f0fafa — tinted bg */
/* Backgrounds */
--eq-bg /* page bg */
--eq-surface /* cards, tables */
--eq-surface-2 /* header rows, modal headers */
/* Text */
--eq-text /* primary */
--eq-text-secondary /* subtitles */
--eq-text-muted /* timestamps, counts */
/* Action button backgrounds */
--eq-btn-create-bg /* #16a34a — green */
--eq-btn-edit-bg /* #2563eb — blue */
--eq-btn-delete-bg /* #dc2626 — red */
--eq-btn-default-bg /* #3a8f8f — teal */CSS utility classes
/* Buttons */
.eq-btn .eq-btn-create /* + Add, Save — green */
.eq-btn .eq-btn-edit /* Edit — blue */
.eq-btn .eq-btn-delete /* Delete — red */
.eq-btn .eq-btn-default /* teal default */
.eq-btn .eq-btn-secondary /* cancel / ghost */
.eq-btn .eq-btn-ghost /* outlined teal */
.eq-btn-sm .eq-btn-lg /* size modifiers */
.eq-btn-icon /* round icon button */
/* Inputs */
.eq-input /* text, email, number … */
.eq-select /* <select> with teal chevron */
.eq-textarea
/* Layout */
.eq-card /* surface card */
.eq-table-wrap /* scrollable table shell */
.eq-table /* <table> */
.eq-toolbar /* flex toolbar row */
.eq-toolbar-left .eq-toolbar-right
/* Modals & slide-overs */
.eq-modal-overlay /* backdrop */
.eq-modal /* modal box */
.eq-modal-header .eq-modal-body .eq-modal-footer
.eq-slideover /* right-side panel */
.eq-slideover-header .eq-slideover-body
/* Feedback */
.eq-badge .eq-badge-brand .eq-badge-success .eq-badge-error .eq-badge-warning
.eq-spinner .eq-spinner-sm .eq-spinner-white
.eq-skeleton /* shimmer animation */
.eq-field-error .eq-field-hint
.eq-query-loading .eq-query-error /* TanStack Query states */Dark mode
<!-- system preference (automatic) -->
<!-- OR force dark: -->
<html data-theme="dark">
<!-- OR via Tailwind: -->
<html class="dark">Built-in toast (no external lib)
import { makeEconneqToast } from '@econneq/headless-crud/adapters/tanstack-query';
const toast = makeEconneqToast();
toast({ title: 'Saved!', variant: 'success' });
toast({ title: 'Oops', description: 'Something went wrong', variant: 'destructive' });
toast({ title: 'Heads up', variant: 'warning', duration: 8000 });
toast({ title: 'FYI', variant: 'default' });Query key factory
import { queryKeys } from '@econneq/headless-crud/adapters/tanstack-query';
const keys = queryKeys('users');
keys.all // ['users']
keys.lists() // ['users', 'list']
keys.list() // ['users', 'list']
keys.list({ role }) // ['users', 'list', { role }]
keys.detail(id) // ['users', 'detail', id]REST mutation helper
import { makeRestMutation } from '@econneq/headless-crud/adapters/tanstack-query';
const mutation = makeRestMutation('/api/users', {
headers: { Authorization: `Bearer ${token}` },
idKey: 'id', // default
deleteMethod: 'DELETE', // or 'PATCH'
});
// Behaviour:
// variables.delete === true → DELETE /api/users/:id
// variables.id present → PUT /api/users/:id
// otherwise → POST /api/usersInvalidation pattern (recommended)
import { useQueryClient, useMutation } from '@tanstack/react-query';
import { queryKeys } from '@econneq/headless-crud/adapters/tanstack-query';
function useUserMutations() {
const qc = useQueryClient();
const keys = queryKeys('users');
return useMutation({
mutationFn: makeRestMutation('/api/users'),
onSuccess: () => qc.invalidateQueries({ queryKey: keys.lists() }),
onError: (err) => console.error(err),
});
}TypeScript
All types are unchanged from v1 plus the new ones below:
import type {
// v1 — unchanged
CrudConfig, CrudEngineProps, FieldConfig, Column, FormMode,
// v2 additions
DefaultTableProps, // now includes isFetching, dataUpdatedAt
} from '@econneq/headless-crud';
import type {
RestCrudMode,
} from '@econneq/headless-crud/adapters/tanstack-query';File structure
src/
├── adapters/
│ ├── apollo.ts ← unchanged from v1
│ ├── fetch.ts ← unchanged from v1
│ └── tanstack-query.ts ← NEW: TanStack Query adapter + helpers
├── components/
│ ├── CrudEngine.tsx
│ ├── DynamicField.tsx ← patched: eq-* CSS vars
│ ├── DynamicForm.tsx
│ ├── FormContainer.tsx ← themed: modal/slideover/inline
│ ├── SearchableCrudEngine.tsx
│ ├── SearchEngine.tsx
│ ├── SearchSelect.tsx
│ └── ExportToolbar.tsx
├── defaults/
│ ├── DefaultTable.tsx ← themed: skeleton, isFetching, dataUpdatedAt
│ └── DefaultConfirmDialog.tsx ← themed: teal accent bar
├── theme/
│ └── theme.ts ← NEW: CSS vars, injectEconneqTheme()
├── hooks/ provider/ types/ validators/ i18n/
└── index.ts ← exports themeE-CONNEQ Technologies — v2.0.0
