najm-kit
v2.1.48
Published
Reusable React UI component package for Najm framework
Readme
najm-kit
Reusable React component library for Najm applications. Provides themed UI primitives, hooks, and form components.
Install
bun add najm-kit tailwindcss @tailwindcss/postcssPeer dependencies: react >=18, react-dom >=18. Requires Tailwind CSS v4 in the host app.
Optional peer dependencies: recharts, @tanstack/react-table, react-hook-form, @tanstack/react-query.
Styling — the entire setup
najm-kit is a Tailwind v4, shadcn-compatible library. PostCSS config (postcss.config.mjs):
export default { plugins: { "@tailwindcss/postcss": {} } };Your global stylesheet — two imports, that's it:
@import "tailwindcss";
@import "najm-kit/theme.css";This gives you every najm-kit component styled, dark mode wired (the .dark class),
and a full token-backed palette you can use in your own markup too
(bg-background, bg-card, bg-primary, text-muted-foreground, border-border, …).
Theming
najm-kit uses the standard shadcn token names (no prefix), so you rebrand by overriding CSS variables — or paste a theme straight from tweakcn / the shadcn registry:
:root { --primary: oklch(0.55 0.2 290); --radius: 0.75rem; }
.dark { --primary: oklch(0.70 0.18 290); }Add your own extra colors alongside najm-kit's:
@theme { --color-success: oklch(0.7 0.18 150); } /* → bg-success, text-success */Dark mode: toggle the dark class on <html> (or any wrapper):
document.documentElement.classList.toggle("dark");Theme Provider (optional)
For scoped theming without writing CSS — useful for embedded surfaces. The provider
is opt-in: with no props it injects nothing and your :root/.dark CSS owns theming.
import { NajmThemeProvider } from 'najm-kit';
// preset:
<NajmThemeProvider preset="dark-blue">{children}</NajmThemeProvider>
// or mode + accent:
<NajmThemeProvider mode="dark" accent="emerald">{children}</NajmThemeProvider>
// shadcn-style global radius scale:
<NajmThemeProvider radius="0.75rem">{children}</NajmThemeProvider>
// exact same radius for cards, tables, buttons, inputs, dialogs, etc.:
<NajmThemeProvider radius="0.75rem">
{children}
</NajmThemeProvider>rounded-full and rounded-none remain explicit, so avatars, pills, switches,
and square variants keep their intended shape.
JSON theme settings
Store one theme object in a JSON file, local storage, or your settings API:
{
"mode": "dark",
"accent": "violet",
"radius": "0.75rem",
"appearance": { "borderWidth": "1px" },
"tokens": {
"primary": "oklch(0.62 0.2 290)",
"primary-foreground": "oklch(1 0 0)",
"sidebar": "oklch(0.18 0.02 290)",
"chart-1": "oklch(0.70 0.20 40)"
}
}Load and apply it from the same settings state used by your theme editor:
import rawTheme from './theme.json';
import { NajmThemeProvider, parseNajmThemeConfig } from 'najm-kit';
const initialTheme = parseNajmThemeConfig(rawTheme);
function App() {
const [theme, setTheme] = useState(initialTheme);
return (
<NajmThemeProvider config={theme}>
<SettingsPage value={theme} onChange={setTheme} />
{children}
</NajmThemeProvider>
);
}Changing the state updates the complete theme immediately. Use
stringifyNajmThemeConfig(theme) when persisting it, and parse settings loaded
from an API or local storage with parseNajmThemeConfig before applying them.
Components
Import from najm-kit:
import { NButton, buttonVariants } from 'najm-kit';
import { Input } from 'najm-kit';
import { Card, CardHeader, CardTitle, CardContent } from 'najm-kit';
import { Dialog, DialogContent, DialogTrigger } from 'najm-kit';
import { DataTable } from 'najm-kit';
import { Form, FormInput, useNForm } from 'najm-kit';Available Primitives
| Category | Components | |----------|-----------| | Actions | NButton, IconButton, toggleVariants | | Forms | Input, Textarea, Label, Select, Checkbox, RadioGroup, Switch, DateInput, FileInput | | Feedback | Alert, Badge, Progress, Spinner, Toast | | Layout | Card, Sheet, Dialog, Popover, DropdownMenu, Tabs | | Data | Table (NTable), StatCard, DetailList | | Overlays | Command palette, Tooltip, Toast |
Hooks
import { useKeyboard } from 'najm-kit';
import { useDelayedLoading } from 'najm-kit';
import { useClickOutside } from 'najm-kit';
import { useDebouncedValue } from 'najm-kit';
import { useInfiniteScroll } from 'najm-kit';
import { useSelection } from 'najm-kit';Production Notes
- Designed for dashboard/admin UIs in Najm-powered applications
- Uses Radix UI primitives under the hood — accessible by default
- All components are unstyled by default — apply
buttonVariants(),badgeVariants(), etc. with Tailwind - Requires Tailwind CSS v4 in the host application (see Styling above)
- CodeMirror components are optional peer deps — import from
najm-kit/jsononly if needed
NTable responsive columns
NTable accepts an NTableColumnDef<T>[]. Each column's meta can carry:
visible?: boolean— app-owned eligibility gate. Defaults totrue. Set this from your role / capability decision. Columns withvisible: falseare removed from headers, body cells, the loading skeleton, and the column-settings menu.hiddenBelow?: "sm" | "md" | "lg" | "xl" | "2xl"— hide the table column below the chosen Tailwind breakpoint. The column remains visible at that breakpoint and above (mobile-first). Table view only.
import { NTable, type NTableColumnDef } from "najm-kit";
const columns: NTableColumnDef<Family>[] = [
{ accessorKey: "name", header: "Family account" },
{
accessorKey: "email",
header: "Email",
meta: {
visible: can("families.email.read"),
hiddenBelow: "lg",
},
},
];Notes:
visibleis application-owned eligibility, not an NTable role system.NTablenever importsnajm-author reads a session; convert your own role / capabilities to a boolean.- Omitting
visibleis the same astrue. hiddenBelowis table-only. Card view, JSON view, and custom modes ignore it. Cards must do their own capability gating insiderenderCard.- Hiding a column is presentation only. The backend must still enforce the permission and privacy-project the field. Never rely on UI hiding to protect sensitive data.
- The user-controlled column visibility menu (settings → Columns) keeps working independently. It can report a column as selected while CSS hides it below the configured breakpoint.
- The columns the TanStack table receives are already filtered, so the
settings menu will not list
visible: falsecolumns.
If you need to inspect or build your own effective column list, the same
pure helper is exported as filterResponsiveColumns. The literal class
map is also exported as hiddenBelowClasses, and
resolveHiddenBelowClass(breakpoint) returns the class for a single
breakpoint or undefined when no breakpoint is set.
NTable responsive cards, loading, and pagination
Responsive row actions are visible by default on phone, tablet, and coarse or
non-hover pointers. Fine-pointer desktop layouts may reveal them on hover, but
keyboard focus always reveals the action. Applications still decide which menu
items exist through menu, onView, onEdit, and onDelete; visibility does
not grant an action or replace server authorization.
When dynamicHeight is enabled, table and card loading skeletons measure the
available body. Table rows use the same header/row geometry as dynamic page
sizing, while cards measure the active grid columns, card height, and gap. The
loading surface also follows the loaded bordered, design recipe, radius,
border color, shadow, and classNames.content/classNames.cards contract.
Use cardPagination to choose pagination presentation whenever the effective
rendered mode is cards:
{ mode: "paged" }(the default) preserves existing pagination.{ mode: "all" }renders every row already supplied and hides the footer.{ mode: "load-more", ... }renders every supplied row and provides a guarded, keyboard-operable Load more/Retry control with polite loading, appended-result, and end-of-list announcements.
showPagination={false} remains an absolute presentation override and hides
both numbered controls and Load more. In table mode, existing controlled and
manual server pagination remains unchanged.
import { NTable, type NTableCardPagination } from "najm-kit";
const cardPagination: NTableCardPagination = {
mode: "load-more",
hasNextPage: query.hasNextPage,
loadingMore: query.isFetchingNextPage,
loadMoreError: query.isFetchNextPageError
? "The next page could not be loaded."
: undefined,
onLoadMore: () => query.fetchNextPage(),
loadMoreLabel: "Load more",
loadingMoreLabel: "Loading more...",
retryLabel: "Retry",
endLabel: "No more results.",
};
<NTable
data={query.data?.pages.flatMap((page) => page.rows) ?? []}
columns={columns}
getRowId={(row) => row.id}
renderCard={ResultCard}
cardPagination={cardPagination}
/>The application owns the query, cursor/offset, accumulated pages, cache invalidation, search/filter/sort semantics, authorization, and privacy projection. Najm Kit never imports React Query, calls an endpoint, invents a page size, or treats supplied rows as proof that every database row is loaded. Client sorting and filtering cover the rows currently supplied unless the application implements matching server-side behavior.
For a responsive screen that uses current-page data in desktop table mode and
accumulated pages in card mode, keep those two query shapes in the application
and pass the appropriate data. Crossing the <640px responsive-card
breakpoint does not overwrite the user's chosen view, pagination position,
sorting, filters, expansion, or row selection.
