najm-kit 2.8.2 → 2.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +71 -27
- package/README.md +477 -237
- package/dist/{NajmUIProvider-BSbXaqak.d.ts → NajmUIProvider-BnReyojl.d.ts} +99 -163
- package/dist/adapters/app.d.ts +5 -1
- package/dist/adapters/app.mjs +3 -3
- package/dist/adapters/next.d.ts +28 -2
- package/dist/adapters/next.mjs +2 -2
- package/dist/chunk-7H6NFBQT.mjs +75 -0
- package/dist/{chunk-IRFFSAO2.mjs → chunk-EUQOTPLV.mjs} +26 -2
- package/dist/{chunk-USZUOJMK.mjs → chunk-FDONJASN.mjs} +163 -3
- package/dist/{chunk-GHMR45H3.mjs → chunk-HML2JZXT.mjs} +1 -1
- package/dist/chunk-JUYT2ISO.mjs +404 -0
- package/dist/{chunk-6OOBAEH2.mjs → chunk-TFHWLE7N.mjs} +1 -403
- package/dist/design-config-D_x1GCu3.d.ts +17 -0
- package/dist/design-types-Rkpt8Pg1.d.ts +159 -0
- package/dist/index.d.ts +60 -77
- package/dist/index.mjs +202 -220
- package/dist/json.mjs +3 -2
- package/dist/server/index.d.ts +3 -0
- package/dist/server/index.mjs +2 -0
- package/dist/server/react.d.ts +16 -0
- package/dist/server/react.mjs +23 -0
- package/dist/server/reactClientGuard.d.ts +2 -0
- package/dist/server/reactClientGuard.mjs +4 -0
- package/dist/uiBootstrap-Ci6K5j5t.d.ts +93 -0
- package/package.json +16 -2
package/README.md
CHANGED
|
@@ -123,7 +123,7 @@ Changing the state updates the complete theme immediately. Use
|
|
|
123
123
|
`stringifyNajmThemeConfig(theme)` when persisting it, and parse settings loaded
|
|
124
124
|
from an API or local storage with `parseNajmThemeConfig` before applying them.
|
|
125
125
|
|
|
126
|
-
## Components
|
|
126
|
+
## Components
|
|
127
127
|
|
|
128
128
|
Import from `najm-kit`:
|
|
129
129
|
|
|
@@ -145,44 +145,172 @@ import { Form, FormInput, useNForm } from 'najm-kit';
|
|
|
145
145
|
| Feedback | Alert, Badge, Progress, Spinner, Toast |
|
|
146
146
|
| Layout | Card, Sheet, Dialog, Popover, DropdownMenu, Tabs |
|
|
147
147
|
| Data | Table (NTable), StatCard, DetailList |
|
|
148
|
-
| Overlays | Command palette, Tooltip, Toast |
|
|
149
|
-
|
|
150
|
-
##
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
<
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
{
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
148
|
+
| Overlays | Command palette, Tooltip, Toast |
|
|
149
|
+
|
|
150
|
+
## Images and avatars
|
|
151
|
+
|
|
152
|
+
Three components, one fallback rule. Each tries its sources in order, tries a
|
|
153
|
+
source at most once, and discards what it knows about a failure the moment the
|
|
154
|
+
sources change.
|
|
155
|
+
|
|
156
|
+
### `NImage` — plain `<img>`
|
|
157
|
+
|
|
158
|
+
For a logo or an icon whose box the caller's CSS already owns. No layout is
|
|
159
|
+
invented, and `onError` is forwarded rather than swallowed.
|
|
160
|
+
|
|
161
|
+
```tsx
|
|
162
|
+
import { NImage } from 'najm-kit';
|
|
163
|
+
|
|
164
|
+
<NImage src={logo} fallback="/brand/logo.svg" alt="Acme" className="h-8 w-auto" />
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### `NAvatar` — person or record
|
|
168
|
+
|
|
169
|
+
The image is a native `<img>` loaded directly by the browser, so a same-origin
|
|
170
|
+
protected route works with the session the page already has, and the package
|
|
171
|
+
needs no knowledge of which routes are protected.
|
|
172
|
+
|
|
173
|
+
```tsx
|
|
174
|
+
import { NAvatar } from 'najm-kit';
|
|
175
|
+
|
|
176
|
+
<NAvatar
|
|
177
|
+
src={member.image}
|
|
178
|
+
fallbackSrc={stockPortrait}
|
|
179
|
+
version={member.imageRevision}
|
|
180
|
+
title={member.name}
|
|
181
|
+
subtitle={member.role}
|
|
182
|
+
size="lg"
|
|
183
|
+
/>
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
- The primary source is tried first, then `fallbackSrc`, then the initials.
|
|
187
|
+
- `version` (or `srcVersion`) is appended as `?v=…` to every remote source, so a
|
|
188
|
+
re-upload is not served from cache. `data:` and `blob:` sources are left alone.
|
|
189
|
+
- Initials stay visible until an image paints and come back if every source
|
|
190
|
+
fails — a transparent PNG never shows letters through itself.
|
|
191
|
+
- `imageProps` reaches the element for `loading`, `sizes`, `crossOrigin`,
|
|
192
|
+
`referrerPolicy`, and the load/error handlers. It defaults to `loading="lazy"`,
|
|
193
|
+
and supplied handlers are composed with the fallback chain rather than
|
|
194
|
+
replacing it.
|
|
195
|
+
|
|
196
|
+
### `NNextImage` — optimized, from `najm-kit/next`
|
|
197
|
+
|
|
198
|
+
Same fallback contract with Next's optimizer, layout reservation, `fill`, and
|
|
199
|
+
`sizes`. It lives only in the `najm-kit/next` entry, because the root package
|
|
200
|
+
stays installable without Next.
|
|
201
|
+
|
|
202
|
+
```tsx
|
|
203
|
+
import { NNextImage } from 'najm-kit/next';
|
|
204
|
+
|
|
205
|
+
// A public asset: let the optimizer resize and re-encode it.
|
|
206
|
+
<NNextImage src="/covers/spring.png" alt="Spring" width={64} height={64} />
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
For an asset the browser must fetch directly — one behind an authenticated route,
|
|
210
|
+
typically — the *application* says so:
|
|
211
|
+
|
|
212
|
+
```tsx
|
|
213
|
+
<NNextImage
|
|
214
|
+
src={record.image}
|
|
215
|
+
alt={record.name}
|
|
216
|
+
fill
|
|
217
|
+
sizes="64px"
|
|
218
|
+
unoptimized
|
|
219
|
+
/>
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
`unoptimized` is passed at the call site rather than inferred from the URL:
|
|
223
|
+
which routes are protected is the application's fact, not something a package
|
|
224
|
+
can read off a path. It changes delivery mechanics only — session validation,
|
|
225
|
+
permissions, privacy projection, and what bytes come back all remain the
|
|
226
|
+
backend's.
|
|
227
|
+
|
|
228
|
+
## Status badges
|
|
229
|
+
|
|
230
|
+
`<NBadge status="…" />` already maps a broad lifecycle vocabulary onto the
|
|
231
|
+
semantic colors, so it is correct without configuration:
|
|
232
|
+
|
|
233
|
+
```tsx
|
|
234
|
+
import { NBadge } from 'najm-kit';
|
|
235
|
+
|
|
236
|
+
<NBadge status="out_for_delivery" /> // warning, "Out For Delivery"
|
|
237
|
+
<NBadge status="nebulous" /> // neutral, "Nebulous"
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
What an application usually adds on top is the same three things at every call
|
|
241
|
+
site: its look, its shape, and its own translated label. Declare them once:
|
|
242
|
+
|
|
243
|
+
```tsx
|
|
244
|
+
<NajmAppProvider
|
|
245
|
+
badgeDefaults={{
|
|
246
|
+
look: 'soft',
|
|
247
|
+
shape: 'pill',
|
|
248
|
+
statusLabelKeys: {
|
|
249
|
+
active: 'status.active',
|
|
250
|
+
out_for_delivery: 'status.outForDelivery',
|
|
251
|
+
},
|
|
252
|
+
}}
|
|
253
|
+
>
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
`badgeDefaults` lives on `NajmUIProvider` and is inherited by
|
|
257
|
+
`NajmNextUIProvider` and `NajmAppProvider`, so there is one place to set it.
|
|
258
|
+
The keys are the application's catalog keys, resolved through the same `t` the
|
|
259
|
+
provider already has — this package ships no status catalog. A language change
|
|
260
|
+
recomputes every label without a remount.
|
|
261
|
+
|
|
262
|
+
Resolution, most specific first:
|
|
263
|
+
|
|
264
|
+
1. An explicit prop beats every provider default.
|
|
265
|
+
2. `label` beats string children; string children beat the provider's label.
|
|
266
|
+
3. A `statusLabels` literal beats a `statusLabelKeys` catalog lookup.
|
|
267
|
+
4. An unmapped status is humanized (`pending_review` → `Pending Review`).
|
|
268
|
+
5. A per-instance `statusMap`/`iconMap` merges over the provider's, so
|
|
269
|
+
overriding one status costs one status.
|
|
270
|
+
6. Provider status defaults apply **only** when `status` is set —
|
|
271
|
+
`<NBadge>Beta</NBadge>` keeps the ordinary content-badge look.
|
|
272
|
+
|
|
273
|
+
Statuses are matched through one rule, exported as `normalizeStatusToken`, so
|
|
274
|
+
`Out-For-Delivery `, `out for delivery`, and `out_for_delivery` are the same
|
|
275
|
+
key for colors, icons, and labels alike. Badge text is presentation: it renames
|
|
276
|
+
nothing in the backend and validates no lifecycle transition.
|
|
277
|
+
|
|
278
|
+
## Global form development tools
|
|
279
|
+
|
|
280
|
+
Enable schema-driven test values once on the full application provider. Every
|
|
281
|
+
`NForm` and `WizardForm` below it then fills from its Zod schema when F8 is
|
|
282
|
+
pressed; applications do not need a second provider or a form-fill helper.
|
|
283
|
+
|
|
284
|
+
```tsx
|
|
285
|
+
import { NajmAppProvider } from "najm-kit/app";
|
|
286
|
+
|
|
287
|
+
<NajmAppProvider formDevTools>
|
|
288
|
+
<App />
|
|
289
|
+
</NajmAppProvider>;
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
Pass a boolean to control it from application settings:
|
|
293
|
+
|
|
294
|
+
```tsx
|
|
295
|
+
<NajmAppProvider formDevTools={formFillEnabled}>
|
|
296
|
+
<App />
|
|
297
|
+
</NajmAppProvider>
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
Forms with live relation options can override only those fields. The provider
|
|
301
|
+
still owns enablement and Najm Kit still owns schema traversal and generation.
|
|
302
|
+
|
|
303
|
+
```tsx
|
|
304
|
+
<NForm
|
|
305
|
+
schema={orderSchema}
|
|
306
|
+
devTools={{ overrides: { customerId: customerOptions } }}
|
|
307
|
+
onSubmit={saveOrder}
|
|
308
|
+
>
|
|
309
|
+
{/* fields */}
|
|
310
|
+
</NForm>
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
## ImageInput and AvatarInput
|
|
186
314
|
|
|
187
315
|
`ImageInput` and `AvatarInput` ship with a resilient preview contract so
|
|
188
316
|
consumers do not need to wrap them with application-specific preview
|
|
@@ -247,93 +375,93 @@ Key behaviors:
|
|
|
247
375
|
a newer value, and object URLs created by the component are tracked so
|
|
248
376
|
consumer-owned blob URLs are never revoked.
|
|
249
377
|
|
|
250
|
-
`AvatarInput` forwards every preview and accessibility prop unchanged while
|
|
251
|
-
preserving its circular, size, fill, and camera-icon defaults.
|
|
252
|
-
|
|
253
|
-
## Formatting
|
|
254
|
-
|
|
255
|
-
Pure formatters are available from the server-safe `najm-kit/format` entry.
|
|
256
|
-
Money values are integer minor units and use the currency's own exponent (for
|
|
257
|
-
example MAD has two decimals, JPY zero, and KWD three).
|
|
258
|
-
|
|
259
|
-
```ts
|
|
260
|
-
import { formatCurrency, formatDate, slugify } from 'najm-kit/format';
|
|
261
|
-
|
|
262
|
-
formatCurrency(12_500, { locale: 'fr-MA', currency: 'MAD' });
|
|
263
|
-
formatDate('2026-08-08T20:00:00Z', {
|
|
264
|
-
locale: 'fr-MA',
|
|
265
|
-
timeZone: 'Africa/Casablanca',
|
|
266
|
-
});
|
|
267
|
-
slugify('Najm Format & Pagination');
|
|
268
|
-
```
|
|
269
|
-
|
|
270
|
-
Client code can use the active locale, time zone, currency, and placeholder
|
|
271
|
-
through `useNajmFormat`. `NajmAppProvider` mounts the format provider for you:
|
|
272
|
-
|
|
273
|
-
```tsx
|
|
274
|
-
import { NajmAppProvider } from 'najm-kit/app';
|
|
275
|
-
import { useNajmFormat } from 'najm-kit';
|
|
276
|
-
|
|
277
|
-
<NajmAppProvider
|
|
278
|
-
translations={translations}
|
|
279
|
-
currency="MAD"
|
|
280
|
-
locales={{ en: 'en-MA', fr: 'fr-MA' }}
|
|
281
|
-
>
|
|
282
|
-
<App />
|
|
283
|
-
</NajmAppProvider>
|
|
284
|
-
|
|
285
|
-
function Total({ value }: { value: number }) {
|
|
286
|
-
return <span>{useNajmFormat().money(value)}</span>;
|
|
287
|
-
}
|
|
288
|
-
```
|
|
289
|
-
|
|
290
|
-
## Offset pagination and queries
|
|
291
|
-
|
|
292
|
-
`najm-kit/pagination` is server-safe and framework-independent. It accepts
|
|
293
|
-
endpoints that return either `{ rows, total }` or a bare row array. When no
|
|
294
|
-
total exists it probes for one extra row; when a total exists continuation is
|
|
295
|
-
calculated without another request.
|
|
296
|
-
|
|
297
|
-
```ts
|
|
298
|
-
import {
|
|
299
|
-
createOffsetPagination,
|
|
300
|
-
fetchOffsetPage,
|
|
301
|
-
} from 'najm-kit/pagination';
|
|
302
|
-
|
|
303
|
-
const pagination = createOffsetPagination(pageIndex, pageSize);
|
|
304
|
-
const page = await fetchOffsetPage(
|
|
305
|
-
({ limit, offset }) => api.orders.list({ limit, offset }),
|
|
306
|
-
pagination,
|
|
307
|
-
);
|
|
308
|
-
```
|
|
309
|
-
|
|
310
|
-
React Query consumers install the optional `@tanstack/react-query` peer and use
|
|
311
|
-
the isolated `najm-kit/query` entry. `useResponsiveOffsetList` resolves numbered
|
|
312
|
-
desktop paging versus card continuation and exposes props that plug directly
|
|
313
|
-
into `NTable` and `createCardPagination`.
|
|
314
|
-
|
|
315
|
-
```tsx
|
|
316
|
-
import { NTable, createCardPagination } from 'najm-kit';
|
|
317
|
-
import { useResponsiveOffsetList } from 'najm-kit/query';
|
|
318
|
-
|
|
319
|
-
const list = useResponsiveOffsetList({
|
|
320
|
-
queryKey: ['orders'],
|
|
321
|
-
fetchPage: ({ limit, offset }) => api.orders.list({ limit, offset }),
|
|
322
|
-
strategy: 'paged',
|
|
323
|
-
});
|
|
324
|
-
|
|
325
|
-
<NTable
|
|
326
|
-
data={list.data}
|
|
327
|
-
columns={columns}
|
|
328
|
-
manualPagination
|
|
329
|
-
pageCount={list.pageCount}
|
|
330
|
-
pagination={list.pagination}
|
|
331
|
-
onPaginationChange={list.onPaginationChange}
|
|
332
|
-
cardPagination={createCardPagination(list, labels)}
|
|
333
|
-
/>
|
|
334
|
-
```
|
|
335
|
-
|
|
336
|
-
## Hooks
|
|
378
|
+
`AvatarInput` forwards every preview and accessibility prop unchanged while
|
|
379
|
+
preserving its circular, size, fill, and camera-icon defaults.
|
|
380
|
+
|
|
381
|
+
## Formatting
|
|
382
|
+
|
|
383
|
+
Pure formatters are available from the server-safe `najm-kit/format` entry.
|
|
384
|
+
Money values are integer minor units and use the currency's own exponent (for
|
|
385
|
+
example MAD has two decimals, JPY zero, and KWD three).
|
|
386
|
+
|
|
387
|
+
```ts
|
|
388
|
+
import { formatCurrency, formatDate, slugify } from 'najm-kit/format';
|
|
389
|
+
|
|
390
|
+
formatCurrency(12_500, { locale: 'fr-MA', currency: 'MAD' });
|
|
391
|
+
formatDate('2026-08-08T20:00:00Z', {
|
|
392
|
+
locale: 'fr-MA',
|
|
393
|
+
timeZone: 'Africa/Casablanca',
|
|
394
|
+
});
|
|
395
|
+
slugify('Najm Format & Pagination');
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
Client code can use the active locale, time zone, currency, and placeholder
|
|
399
|
+
through `useNajmFormat`. `NajmAppProvider` mounts the format provider for you:
|
|
400
|
+
|
|
401
|
+
```tsx
|
|
402
|
+
import { NajmAppProvider } from 'najm-kit/app';
|
|
403
|
+
import { useNajmFormat } from 'najm-kit';
|
|
404
|
+
|
|
405
|
+
<NajmAppProvider
|
|
406
|
+
translations={translations}
|
|
407
|
+
currency="MAD"
|
|
408
|
+
locales={{ en: 'en-MA', fr: 'fr-MA' }}
|
|
409
|
+
>
|
|
410
|
+
<App />
|
|
411
|
+
</NajmAppProvider>
|
|
412
|
+
|
|
413
|
+
function Total({ value }: { value: number }) {
|
|
414
|
+
return <span>{useNajmFormat().money(value)}</span>;
|
|
415
|
+
}
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
## Offset pagination and queries
|
|
419
|
+
|
|
420
|
+
`najm-kit/pagination` is server-safe and framework-independent. It accepts
|
|
421
|
+
endpoints that return either `{ rows, total }` or a bare row array. When no
|
|
422
|
+
total exists it probes for one extra row; when a total exists continuation is
|
|
423
|
+
calculated without another request.
|
|
424
|
+
|
|
425
|
+
```ts
|
|
426
|
+
import {
|
|
427
|
+
createOffsetPagination,
|
|
428
|
+
fetchOffsetPage,
|
|
429
|
+
} from 'najm-kit/pagination';
|
|
430
|
+
|
|
431
|
+
const pagination = createOffsetPagination(pageIndex, pageSize);
|
|
432
|
+
const page = await fetchOffsetPage(
|
|
433
|
+
({ limit, offset }) => api.orders.list({ limit, offset }),
|
|
434
|
+
pagination,
|
|
435
|
+
);
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
React Query consumers install the optional `@tanstack/react-query` peer and use
|
|
439
|
+
the isolated `najm-kit/query` entry. `useResponsiveOffsetList` resolves numbered
|
|
440
|
+
desktop paging versus card continuation and exposes props that plug directly
|
|
441
|
+
into `NTable` and `createCardPagination`.
|
|
442
|
+
|
|
443
|
+
```tsx
|
|
444
|
+
import { NTable, createCardPagination } from 'najm-kit';
|
|
445
|
+
import { useResponsiveOffsetList } from 'najm-kit/query';
|
|
446
|
+
|
|
447
|
+
const list = useResponsiveOffsetList({
|
|
448
|
+
queryKey: ['orders'],
|
|
449
|
+
fetchPage: ({ limit, offset }) => api.orders.list({ limit, offset }),
|
|
450
|
+
strategy: 'paged',
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
<NTable
|
|
454
|
+
data={list.data}
|
|
455
|
+
columns={columns}
|
|
456
|
+
manualPagination
|
|
457
|
+
pageCount={list.pageCount}
|
|
458
|
+
pagination={list.pagination}
|
|
459
|
+
onPaginationChange={list.onPaginationChange}
|
|
460
|
+
cardPagination={createCardPagination(list, labels)}
|
|
461
|
+
/>
|
|
462
|
+
```
|
|
463
|
+
|
|
464
|
+
## Hooks
|
|
337
465
|
|
|
338
466
|
```tsx
|
|
339
467
|
import { useKeyboard } from 'najm-kit';
|
|
@@ -397,116 +525,116 @@ Notes:
|
|
|
397
525
|
- The columns the TanStack table receives are already filtered, so the
|
|
398
526
|
settings menu will not list `visible: false` columns.
|
|
399
527
|
|
|
400
|
-
If you need to inspect or build your own effective column list, the same
|
|
401
|
-
pure helper is exported as `filterResponsiveColumns`. The literal class
|
|
402
|
-
map is also exported as `hiddenBelowClasses`, and
|
|
403
|
-
`resolveHiddenBelowClass(breakpoint)` returns the class for a single
|
|
404
|
-
breakpoint or `undefined` when no breakpoint is set.
|
|
405
|
-
|
|
406
|
-
## NTable responsive cards, loading, and pagination
|
|
407
|
-
|
|
408
|
-
Responsive row actions are visible by default on phone, tablet, and coarse or
|
|
409
|
-
non-hover pointers. Fine-pointer desktop layouts may reveal them on hover, but
|
|
410
|
-
keyboard focus always reveals the action. Applications still decide which menu
|
|
411
|
-
items exist through `menu`, `onView`, `onEdit`, and `onDelete`; visibility does
|
|
412
|
-
not grant an action or replace server authorization.
|
|
413
|
-
|
|
414
|
-
When `dynamicHeight` is enabled, table and card loading skeletons measure the
|
|
415
|
-
available body. Table rows use the same header/row geometry as dynamic page
|
|
416
|
-
sizing, while cards measure the active grid columns, card height, and gap. The
|
|
417
|
-
loading surface also follows the loaded `bordered`, design recipe, radius,
|
|
418
|
-
border color, shadow, and `classNames.content`/`classNames.cards` contract.
|
|
419
|
-
|
|
420
|
-
Use `cardPagination` to choose pagination presentation whenever the effective
|
|
421
|
-
rendered mode is cards:
|
|
422
|
-
|
|
423
|
-
- `{ mode: "paged" }` (the default) preserves existing pagination.
|
|
424
|
-
- `{ mode: "all" }` renders every row already supplied and hides the footer.
|
|
425
|
-
- `{ mode: "load-more", ... }` renders every supplied row and provides a
|
|
426
|
-
guarded, keyboard-operable Load more/Retry control with polite loading,
|
|
427
|
-
appended-result, and end-of-list announcements.
|
|
428
|
-
|
|
429
|
-
`showPagination={false}` remains an absolute presentation override and hides
|
|
430
|
-
both numbered controls and Load more. In table mode, existing controlled and
|
|
431
|
-
manual server pagination remains unchanged.
|
|
432
|
-
|
|
433
|
-
```tsx
|
|
434
|
-
import { NTable, type NTableCardPagination } from "najm-kit";
|
|
435
|
-
|
|
436
|
-
const cardPagination: NTableCardPagination = {
|
|
437
|
-
mode: "load-more",
|
|
438
|
-
hasNextPage: query.hasNextPage,
|
|
439
|
-
loadingMore: query.isFetchingNextPage,
|
|
440
|
-
loadMoreError: query.isFetchNextPageError
|
|
441
|
-
? "The next page could not be loaded."
|
|
442
|
-
: undefined,
|
|
443
|
-
onLoadMore: () => query.fetchNextPage(),
|
|
444
|
-
loadMoreLabel: "Load more",
|
|
445
|
-
loadingMoreLabel: "Loading more...",
|
|
446
|
-
retryLabel: "Retry",
|
|
447
|
-
endLabel: "No more results.",
|
|
448
|
-
};
|
|
449
|
-
|
|
450
|
-
<NTable
|
|
451
|
-
data={query.data?.pages.flatMap((page) => page.rows) ?? []}
|
|
452
|
-
columns={columns}
|
|
453
|
-
getRowId={(row) => row.id}
|
|
454
|
-
renderCard={ResultCard}
|
|
455
|
-
cardPagination={cardPagination}
|
|
456
|
-
/>
|
|
457
|
-
```
|
|
458
|
-
|
|
459
|
-
The application owns the query, cursor/offset, accumulated pages, cache
|
|
460
|
-
invalidation, search/filter/sort semantics, authorization, and privacy
|
|
461
|
-
projection. Najm Kit never imports React Query, calls an endpoint, invents a
|
|
462
|
-
page size, or treats supplied rows as proof that every database row is loaded.
|
|
463
|
-
Client sorting and filtering cover the rows currently supplied unless the
|
|
464
|
-
application implements matching server-side behavior.
|
|
465
|
-
|
|
466
|
-
For a responsive screen that uses current-page data in desktop table mode and
|
|
467
|
-
accumulated pages in card mode, keep those two query shapes in the application
|
|
468
|
-
and pass the appropriate `data`. Crossing the `<640px` responsive-card
|
|
469
|
-
breakpoint does not overwrite the user's chosen view, pagination position,
|
|
470
|
-
sorting, filters, expansion, or row selection.
|
|
471
|
-
|
|
472
|
-
## Theme-backed charts
|
|
473
|
-
|
|
474
|
-
`NBarChart`, `NLineChart`, `NPieChart`, and `NStatusBreakdown` accept generic
|
|
475
|
-
caller-formatted data and use `--chart-1` through `--chart-5` by default.
|
|
476
|
-
Colors repeat deterministically after the fifth series or item; set `color` on
|
|
477
|
-
an exceptional series/item to override that one value. Each chart accepts
|
|
478
|
-
`loading`/`loadingLabel` and renders an accessible shape-matched skeleton.
|
|
479
|
-
`NPieChart` and `NDonutCard` accept `size="sm" | "md" | "lg"` or a numeric
|
|
480
|
-
pixel diameter and shrink within narrow containers.
|
|
481
|
-
|
|
482
|
-
```tsx
|
|
483
|
-
import { NBarChart, NPieChart } from "najm-kit";
|
|
484
|
-
|
|
485
|
-
const data = [
|
|
486
|
-
{ id: "jan", label: "Jan", values: { received: 12, refunded: 2 } },
|
|
487
|
-
{ id: "feb", label: "Feb", values: { received: 18, refunded: 1 } },
|
|
488
|
-
];
|
|
489
|
-
|
|
490
|
-
<NBarChart
|
|
491
|
-
title="Monthly activity"
|
|
492
|
-
data={data}
|
|
493
|
-
series={[
|
|
494
|
-
{ id: "received", label: "Received" },
|
|
495
|
-
{ id: "refunded", label: "Refunded" },
|
|
496
|
-
]}
|
|
497
|
-
valueFormatter={(value) => `${value} MAD`}
|
|
498
|
-
/>
|
|
499
|
-
|
|
500
|
-
<NPieChart
|
|
501
|
-
title="Status"
|
|
502
|
-
size={132}
|
|
503
|
-
items={[
|
|
504
|
-
{ id: "active", label: "Active", value: 8 },
|
|
505
|
-
{ id: "pending", label: "Pending", value: 3 },
|
|
506
|
-
]}
|
|
507
|
-
/>
|
|
508
|
-
```
|
|
509
|
-
|
|
528
|
+
If you need to inspect or build your own effective column list, the same
|
|
529
|
+
pure helper is exported as `filterResponsiveColumns`. The literal class
|
|
530
|
+
map is also exported as `hiddenBelowClasses`, and
|
|
531
|
+
`resolveHiddenBelowClass(breakpoint)` returns the class for a single
|
|
532
|
+
breakpoint or `undefined` when no breakpoint is set.
|
|
533
|
+
|
|
534
|
+
## NTable responsive cards, loading, and pagination
|
|
535
|
+
|
|
536
|
+
Responsive row actions are visible by default on phone, tablet, and coarse or
|
|
537
|
+
non-hover pointers. Fine-pointer desktop layouts may reveal them on hover, but
|
|
538
|
+
keyboard focus always reveals the action. Applications still decide which menu
|
|
539
|
+
items exist through `menu`, `onView`, `onEdit`, and `onDelete`; visibility does
|
|
540
|
+
not grant an action or replace server authorization.
|
|
541
|
+
|
|
542
|
+
When `dynamicHeight` is enabled, table and card loading skeletons measure the
|
|
543
|
+
available body. Table rows use the same header/row geometry as dynamic page
|
|
544
|
+
sizing, while cards measure the active grid columns, card height, and gap. The
|
|
545
|
+
loading surface also follows the loaded `bordered`, design recipe, radius,
|
|
546
|
+
border color, shadow, and `classNames.content`/`classNames.cards` contract.
|
|
547
|
+
|
|
548
|
+
Use `cardPagination` to choose pagination presentation whenever the effective
|
|
549
|
+
rendered mode is cards:
|
|
550
|
+
|
|
551
|
+
- `{ mode: "paged" }` (the default) preserves existing pagination.
|
|
552
|
+
- `{ mode: "all" }` renders every row already supplied and hides the footer.
|
|
553
|
+
- `{ mode: "load-more", ... }` renders every supplied row and provides a
|
|
554
|
+
guarded, keyboard-operable Load more/Retry control with polite loading,
|
|
555
|
+
appended-result, and end-of-list announcements.
|
|
556
|
+
|
|
557
|
+
`showPagination={false}` remains an absolute presentation override and hides
|
|
558
|
+
both numbered controls and Load more. In table mode, existing controlled and
|
|
559
|
+
manual server pagination remains unchanged.
|
|
560
|
+
|
|
561
|
+
```tsx
|
|
562
|
+
import { NTable, type NTableCardPagination } from "najm-kit";
|
|
563
|
+
|
|
564
|
+
const cardPagination: NTableCardPagination = {
|
|
565
|
+
mode: "load-more",
|
|
566
|
+
hasNextPage: query.hasNextPage,
|
|
567
|
+
loadingMore: query.isFetchingNextPage,
|
|
568
|
+
loadMoreError: query.isFetchNextPageError
|
|
569
|
+
? "The next page could not be loaded."
|
|
570
|
+
: undefined,
|
|
571
|
+
onLoadMore: () => query.fetchNextPage(),
|
|
572
|
+
loadMoreLabel: "Load more",
|
|
573
|
+
loadingMoreLabel: "Loading more...",
|
|
574
|
+
retryLabel: "Retry",
|
|
575
|
+
endLabel: "No more results.",
|
|
576
|
+
};
|
|
577
|
+
|
|
578
|
+
<NTable
|
|
579
|
+
data={query.data?.pages.flatMap((page) => page.rows) ?? []}
|
|
580
|
+
columns={columns}
|
|
581
|
+
getRowId={(row) => row.id}
|
|
582
|
+
renderCard={ResultCard}
|
|
583
|
+
cardPagination={cardPagination}
|
|
584
|
+
/>
|
|
585
|
+
```
|
|
586
|
+
|
|
587
|
+
The application owns the query, cursor/offset, accumulated pages, cache
|
|
588
|
+
invalidation, search/filter/sort semantics, authorization, and privacy
|
|
589
|
+
projection. Najm Kit never imports React Query, calls an endpoint, invents a
|
|
590
|
+
page size, or treats supplied rows as proof that every database row is loaded.
|
|
591
|
+
Client sorting and filtering cover the rows currently supplied unless the
|
|
592
|
+
application implements matching server-side behavior.
|
|
593
|
+
|
|
594
|
+
For a responsive screen that uses current-page data in desktop table mode and
|
|
595
|
+
accumulated pages in card mode, keep those two query shapes in the application
|
|
596
|
+
and pass the appropriate `data`. Crossing the `<640px` responsive-card
|
|
597
|
+
breakpoint does not overwrite the user's chosen view, pagination position,
|
|
598
|
+
sorting, filters, expansion, or row selection.
|
|
599
|
+
|
|
600
|
+
## Theme-backed charts
|
|
601
|
+
|
|
602
|
+
`NBarChart`, `NLineChart`, `NPieChart`, and `NStatusBreakdown` accept generic
|
|
603
|
+
caller-formatted data and use `--chart-1` through `--chart-5` by default.
|
|
604
|
+
Colors repeat deterministically after the fifth series or item; set `color` on
|
|
605
|
+
an exceptional series/item to override that one value. Each chart accepts
|
|
606
|
+
`loading`/`loadingLabel` and renders an accessible shape-matched skeleton.
|
|
607
|
+
`NPieChart` and `NDonutCard` accept `size="sm" | "md" | "lg"` or a numeric
|
|
608
|
+
pixel diameter and shrink within narrow containers.
|
|
609
|
+
|
|
610
|
+
```tsx
|
|
611
|
+
import { NBarChart, NPieChart } from "najm-kit";
|
|
612
|
+
|
|
613
|
+
const data = [
|
|
614
|
+
{ id: "jan", label: "Jan", values: { received: 12, refunded: 2 } },
|
|
615
|
+
{ id: "feb", label: "Feb", values: { received: 18, refunded: 1 } },
|
|
616
|
+
];
|
|
617
|
+
|
|
618
|
+
<NBarChart
|
|
619
|
+
title="Monthly activity"
|
|
620
|
+
data={data}
|
|
621
|
+
series={[
|
|
622
|
+
{ id: "received", label: "Received" },
|
|
623
|
+
{ id: "refunded", label: "Refunded" },
|
|
624
|
+
]}
|
|
625
|
+
valueFormatter={(value) => `${value} MAD`}
|
|
626
|
+
/>
|
|
627
|
+
|
|
628
|
+
<NPieChart
|
|
629
|
+
title="Status"
|
|
630
|
+
size={132}
|
|
631
|
+
items={[
|
|
632
|
+
{ id: "active", label: "Active", value: 8 },
|
|
633
|
+
{ id: "pending", label: "Pending", value: 3 },
|
|
634
|
+
]}
|
|
635
|
+
/>
|
|
636
|
+
```
|
|
637
|
+
|
|
510
638
|
### Server-backed combobox search
|
|
511
639
|
|
|
512
640
|
`ComboboxInput` and `FormInput type="combobox"` can delegate filtering to a
|
|
@@ -600,4 +728,116 @@ only, after a real `image` and before the role's gender variant:
|
|
|
600
728
|
```ts
|
|
601
729
|
getPersonImage({ image: child.image, role: "child", gender: child.gender, fallback: child.placeholder });
|
|
602
730
|
```
|
|
603
|
-
|
|
731
|
+
|
|
732
|
+
|
|
733
|
+
## Server UI bootstrap (`najm-kit/server`, `najm-kit/server/react`)
|
|
734
|
+
|
|
735
|
+
An application that renders its own theme and its own logos on the server ends
|
|
736
|
+
up writing the same module every time: fetch the public endpoints, unwrap the
|
|
737
|
+
`data` envelope, validate the payload, fall back to the built-in assets when
|
|
738
|
+
any of that fails, and run the resources in parallel. These two entries own
|
|
739
|
+
that mechanism. What stays with the application is what is genuinely
|
|
740
|
+
application-specific — how a request reaches its own backend, which paths it
|
|
741
|
+
serves, what a valid payload looks like, what the factory values are, and where
|
|
742
|
+
a diagnostic goes.
|
|
743
|
+
|
|
744
|
+
Neither entry is re-exported from `najm-kit`, `najm-kit/next`, or
|
|
745
|
+
`najm-kit/app`. `najm-kit/server` imports no React at all, so a route handler
|
|
746
|
+
or a plain script can use it.
|
|
747
|
+
|
|
748
|
+
### The application's one server module
|
|
749
|
+
|
|
750
|
+
```ts
|
|
751
|
+
// src/lib/serverLoader.ts
|
|
752
|
+
import "server-only";
|
|
753
|
+
|
|
754
|
+
import { parseNajmDesignConfig } from "najm-kit/server";
|
|
755
|
+
import { createReactServerUiBootstrap } from "najm-kit/server/react";
|
|
756
|
+
|
|
757
|
+
export const serverUi = createReactServerUiBootstrap({
|
|
758
|
+
fetcher: async (path) => {
|
|
759
|
+
const { server } = await import("@app/server");
|
|
760
|
+
return server.fetch(new Request(`http://internal${path}`));
|
|
761
|
+
},
|
|
762
|
+
resources: {
|
|
763
|
+
appearance: {
|
|
764
|
+
path: "/api/appearance",
|
|
765
|
+
parse: parseAppearance, // returns undefined or throws to reject
|
|
766
|
+
fallback: getFactoryAppearance, // called per load
|
|
767
|
+
},
|
|
768
|
+
branding: {
|
|
769
|
+
path: "/api/branding",
|
|
770
|
+
parse: parseBranding,
|
|
771
|
+
fallback: getFactoryBranding,
|
|
772
|
+
},
|
|
773
|
+
},
|
|
774
|
+
onDiagnostic: (diagnostic) => {
|
|
775
|
+
console.warn(`[ui-bootstrap] ${diagnostic.resource} ${diagnostic.reason}`, diagnostic);
|
|
776
|
+
},
|
|
777
|
+
});
|
|
778
|
+
|
|
779
|
+
export const loadServerUiBootstrap = serverUi.load;
|
|
780
|
+
export const { appearance: loadServerAppearance, branding: loadServerBranding } =
|
|
781
|
+
serverUi.loaders;
|
|
782
|
+
```
|
|
783
|
+
|
|
784
|
+
`load()` resolves every resource; `loaders.<name>()` and `loadResource(name)`
|
|
785
|
+
read one off the same resolution. Resource names, payload types, and the number
|
|
786
|
+
of resources are the application's — the snapshot type is inferred from the
|
|
787
|
+
`resources` object, so `snapshot.branding` is your branding type and not a
|
|
788
|
+
package interface.
|
|
789
|
+
|
|
790
|
+
### Call the factory once, at module scope
|
|
791
|
+
|
|
792
|
+
`createReactServerUiBootstrap()` builds one `React.cache()` entry. Calling it
|
|
793
|
+
inside a layout, page, or component builds a fresh one per call and shares
|
|
794
|
+
nothing. Every server boundary in a render must import the same module.
|
|
795
|
+
|
|
796
|
+
The cache is React's, so it is request-scoped and nothing else: separate
|
|
797
|
+
requests never see each other's snapshot or each other's failure, and a
|
|
798
|
+
transient outage is retried on the next request rather than pinned into a
|
|
799
|
+
process-global. That also rules out a module `Map`, a module promise,
|
|
800
|
+
`unstable_cache`, `"use cache"`, or a durable cache here — every one of them
|
|
801
|
+
would leak one visitor's render into another's.
|
|
802
|
+
|
|
803
|
+
The snapshot is deliberately stable for the length of one render. A settings
|
|
804
|
+
surface that saves appearance or branding updates the client provider and then
|
|
805
|
+
refreshes or navigates into a new render to observe the persisted result.
|
|
806
|
+
|
|
807
|
+
Outside a render — route handlers, server actions, scripts — use
|
|
808
|
+
`createUiBootstrapLoader()` from `najm-kit/server` directly. There is no request
|
|
809
|
+
cache for `cache()` to write to there, so the adapter would silently re-fetch
|
|
810
|
+
per call.
|
|
811
|
+
|
|
812
|
+
### Failure behaviour
|
|
813
|
+
|
|
814
|
+
Resources fall back independently: a branding outage never discards a valid
|
|
815
|
+
appearance. Each failure calls `onDiagnostic` once with a `reason` of
|
|
816
|
+
`fetch-failed`, `response-not-ok`, `invalid-json`, `invalid-envelope`, or
|
|
817
|
+
`invalid-payload`, plus the path and — for a non-success response — the status.
|
|
818
|
+
Diagnostics never carry response bodies, headers, cookies, or raw thrown
|
|
819
|
+
values; `error` is a normalized `"<name>: <message>"` for an `Error` and the
|
|
820
|
+
value's type for anything else.
|
|
821
|
+
|
|
822
|
+
A `fallback()` that throws is **not** caught. A missing factory theme is the
|
|
823
|
+
application's configuration error, and a second fallback would only hide it.
|
|
824
|
+
|
|
825
|
+
Falling back is right for *public* appearance and branding, where the worst case
|
|
826
|
+
is a visitor seeing the built-in logo. It is not a general rule: do not route
|
|
827
|
+
authenticated, financial, or privacy-sensitive reads through this, because a
|
|
828
|
+
silent fallback there hides an outage behind plausible-looking data.
|
|
829
|
+
|
|
830
|
+
### Envelopes
|
|
831
|
+
|
|
832
|
+
`select` defaults to Najm's `{ data }` envelope. Applications behind a different
|
|
833
|
+
envelope pass their own at the loader level or per resource; returning the
|
|
834
|
+
payload unchanged is a valid selector, and throwing rejects the response as
|
|
835
|
+
`invalid-envelope`.
|
|
836
|
+
|
|
837
|
+
### Client Components
|
|
838
|
+
|
|
839
|
+
`najm-kit/server/react` maps the `browser` export condition to a module that
|
|
840
|
+
throws, so importing it from a Client Component fails the build with an
|
|
841
|
+
explanation rather than shipping the application's fetcher and factory values
|
|
842
|
+
into a browser bundle. Seed the client from the server snapshot through
|
|
843
|
+
`NajmAppProvider` instead.
|