najm-kit 2.7.2 → 2.8.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 +20 -3
- package/README.md +87 -4
- package/dist/{NajmUIProvider-ClpHD-55.d.ts → NajmUIProvider-BSbXaqak.d.ts} +1 -1
- package/dist/adapters/app.d.ts +2 -2
- package/dist/adapters/app.mjs +2 -1
- package/dist/adapters/next.d.ts +2 -2
- package/dist/cardPagination-Bwf3tEeN.d.ts +65 -0
- package/dist/chunk-2NX2VKS2.mjs +42 -0
- package/dist/chunk-F5KXJCCJ.mjs +1 -0
- package/dist/chunk-JABLSOQN.mjs +114 -0
- package/dist/chunk-OUS7OYVA.mjs +54 -0
- package/dist/{chunk-QOP7L6RD.mjs → chunk-PCPMAEKP.mjs} +2 -104
- package/dist/format.d.ts +86 -0
- package/dist/format.mjs +2 -0
- package/dist/index.d.ts +61 -53
- package/dist/index.mjs +100 -21
- package/dist/{pagination-BvukSZir.d.ts → pagination.d.ts} +1 -53
- package/dist/pagination.mjs +1 -0
- package/dist/{paginationLabels-CY2PvbMj.d.ts → paginationLabels-dZLSNxfo.d.ts} +9 -2
- package/dist/query.d.ts +5 -3
- package/dist/query.mjs +4 -2
- package/package.json +11 -1
- package/dist/chunk-7J7DWGKB.mjs +0 -93
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,23 @@
|
|
|
1
|
-
# Changelog
|
|
2
|
-
|
|
3
|
-
## 2.
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 2.8.0 - 2026-08-08
|
|
4
|
+
|
|
5
|
+
- Added server-safe `najm-kit/format` helpers for currency minor units,
|
|
6
|
+
numbers, percentages, dates, times, relative time, tokens, local date inputs,
|
|
7
|
+
and slugs. Client applications can use the same contract reactively through
|
|
8
|
+
`NajmFormatProvider` and `useNajmFormat`.
|
|
9
|
+
- Added the server-safe `najm-kit/pagination` offset protocol, including bounded
|
|
10
|
+
page creation, total-aware continuation, probe-row continuation for APIs
|
|
11
|
+
without totals, and query cleanup that preserves meaningful `false` and `0`.
|
|
12
|
+
- Added the optional-peer `najm-kit/query` entry with offset infinite-query and
|
|
13
|
+
responsive paged/card-list hooks, plus shared card-pagination adapters and
|
|
14
|
+
localized continuation labels.
|
|
15
|
+
- Extended `NajmAppProvider` with formatting locale, currency, and placeholder
|
|
16
|
+
configuration so applications can bind language, time zone, and formatting
|
|
17
|
+
without another host bridge provider.
|
|
18
|
+
- Added shared media-query/card-viewport helpers and avatar-source utilities.
|
|
19
|
+
|
|
20
|
+
## 2.6.2
|
|
4
21
|
|
|
5
22
|
- Added `NSidebarProvider` and `useNSidebar`, so sidebar state can be read from a distance. `NSidebar` renders beside the page content rather than around it, which left applications hand-rolling a context to hand `setMobileOpen` down to a page header — a wrapper component plus an aliased import at every call site. Wrap the shell in `NSidebarProvider` and `NPageHeader` now resolves both `onSidebarOpen` and `mobileBreakpoint` from it, so a header nested anywhere below renders a working mobile trigger with no props threaded to it. Also exports the `NSidebarContextValue` type.
|
|
6
23
|
- `NSidebar` resolves its open and collapsed state as explicit prop → surrounding provider → internal state. Passing `collapsed`, `mobileOpen`, `onCollapsedChange`, or `onMobileOpenChange` keeps behaving exactly as before, and a sidebar with no provider around it still owns its own state, so this is additive for every existing consumer.
|
package/README.md
CHANGED
|
@@ -212,10 +212,93 @@ Key behaviors:
|
|
|
212
212
|
a newer value, and object URLs created by the component are tracked so
|
|
213
213
|
consumer-owned blob URLs are never revoked.
|
|
214
214
|
|
|
215
|
-
`AvatarInput` forwards every preview and accessibility prop unchanged while
|
|
216
|
-
preserving its circular, size, fill, and camera-icon defaults.
|
|
217
|
-
|
|
218
|
-
##
|
|
215
|
+
`AvatarInput` forwards every preview and accessibility prop unchanged while
|
|
216
|
+
preserving its circular, size, fill, and camera-icon defaults.
|
|
217
|
+
|
|
218
|
+
## Formatting
|
|
219
|
+
|
|
220
|
+
Pure formatters are available from the server-safe `najm-kit/format` entry.
|
|
221
|
+
Money values are integer minor units and use the currency's own exponent (for
|
|
222
|
+
example MAD has two decimals, JPY zero, and KWD three).
|
|
223
|
+
|
|
224
|
+
```ts
|
|
225
|
+
import { formatCurrency, formatDate, slugify } from 'najm-kit/format';
|
|
226
|
+
|
|
227
|
+
formatCurrency(12_500, { locale: 'fr-MA', currency: 'MAD' });
|
|
228
|
+
formatDate('2026-08-08T20:00:00Z', {
|
|
229
|
+
locale: 'fr-MA',
|
|
230
|
+
timeZone: 'Africa/Casablanca',
|
|
231
|
+
});
|
|
232
|
+
slugify('Najm Format & Pagination');
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
Client code can use the active locale, time zone, currency, and placeholder
|
|
236
|
+
through `useNajmFormat`. `NajmAppProvider` mounts the format provider for you:
|
|
237
|
+
|
|
238
|
+
```tsx
|
|
239
|
+
import { NajmAppProvider } from 'najm-kit/app';
|
|
240
|
+
import { useNajmFormat } from 'najm-kit';
|
|
241
|
+
|
|
242
|
+
<NajmAppProvider
|
|
243
|
+
translations={translations}
|
|
244
|
+
currency="MAD"
|
|
245
|
+
locales={{ en: 'en-MA', fr: 'fr-MA' }}
|
|
246
|
+
>
|
|
247
|
+
<App />
|
|
248
|
+
</NajmAppProvider>
|
|
249
|
+
|
|
250
|
+
function Total({ value }: { value: number }) {
|
|
251
|
+
return <span>{useNajmFormat().money(value)}</span>;
|
|
252
|
+
}
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
## Offset pagination and queries
|
|
256
|
+
|
|
257
|
+
`najm-kit/pagination` is server-safe and framework-independent. It accepts
|
|
258
|
+
endpoints that return either `{ rows, total }` or a bare row array. When no
|
|
259
|
+
total exists it probes for one extra row; when a total exists continuation is
|
|
260
|
+
calculated without another request.
|
|
261
|
+
|
|
262
|
+
```ts
|
|
263
|
+
import {
|
|
264
|
+
createOffsetPagination,
|
|
265
|
+
fetchOffsetPage,
|
|
266
|
+
} from 'najm-kit/pagination';
|
|
267
|
+
|
|
268
|
+
const pagination = createOffsetPagination(pageIndex, pageSize);
|
|
269
|
+
const page = await fetchOffsetPage(
|
|
270
|
+
({ limit, offset }) => api.orders.list({ limit, offset }),
|
|
271
|
+
pagination,
|
|
272
|
+
);
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
React Query consumers install the optional `@tanstack/react-query` peer and use
|
|
276
|
+
the isolated `najm-kit/query` entry. `useResponsiveOffsetList` resolves numbered
|
|
277
|
+
desktop paging versus card continuation and exposes props that plug directly
|
|
278
|
+
into `NTable` and `createCardPagination`.
|
|
279
|
+
|
|
280
|
+
```tsx
|
|
281
|
+
import { NTable, createCardPagination } from 'najm-kit';
|
|
282
|
+
import { useResponsiveOffsetList } from 'najm-kit/query';
|
|
283
|
+
|
|
284
|
+
const list = useResponsiveOffsetList({
|
|
285
|
+
queryKey: ['orders'],
|
|
286
|
+
fetchPage: ({ limit, offset }) => api.orders.list({ limit, offset }),
|
|
287
|
+
strategy: 'paged',
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
<NTable
|
|
291
|
+
data={list.data}
|
|
292
|
+
columns={columns}
|
|
293
|
+
manualPagination
|
|
294
|
+
pageCount={list.pageCount}
|
|
295
|
+
pagination={list.pagination}
|
|
296
|
+
onPaginationChange={list.onPaginationChange}
|
|
297
|
+
cardPagination={createCardPagination(list, labels)}
|
|
298
|
+
/>
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
## Hooks
|
|
219
302
|
|
|
220
303
|
```tsx
|
|
221
304
|
import { useKeyboard } from 'najm-kit';
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
2
|
import * as React$1 from 'react';
|
|
3
3
|
import React__default from 'react';
|
|
4
|
-
import {
|
|
4
|
+
import { b as NTablePaginationLabels, N as NajmTranslate } from './paginationLabels-dZLSNxfo.js';
|
|
5
5
|
|
|
6
6
|
type NajmMode = 'light' | 'dark';
|
|
7
7
|
type NajmAccent = 'neutral' | 'emerald' | 'green' | 'slate' | 'blue' | 'violet';
|
package/dist/adapters/app.d.ts
CHANGED
|
@@ -3,8 +3,8 @@ import { Translations } from 'najm-i18n';
|
|
|
3
3
|
import { N as NBrandingInput } from '../NBrandingContext-dkADu8JS.js';
|
|
4
4
|
import { NajmNextUIProviderProps } from './next.js';
|
|
5
5
|
import 'react';
|
|
6
|
-
import '../NajmUIProvider-
|
|
7
|
-
import '../paginationLabels-
|
|
6
|
+
import '../NajmUIProvider-BSbXaqak.js';
|
|
7
|
+
import '../paginationLabels-dZLSNxfo.js';
|
|
8
8
|
|
|
9
9
|
/** Branding shown by the kit's chrome. Purely presentational values. */
|
|
10
10
|
interface NajmAppBranding {
|
package/dist/adapters/app.mjs
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
'use client';
|
|
2
|
-
import { NBrandingStateProvider, NajmFormatProvider } from '../chunk-
|
|
2
|
+
import { NBrandingStateProvider, NajmFormatProvider } from '../chunk-PCPMAEKP.mjs';
|
|
3
3
|
import { NajmNextUIProvider } from '../chunk-IRFFSAO2.mjs';
|
|
4
4
|
import '../chunk-USZUOJMK.mjs';
|
|
5
5
|
import '../chunk-KVZACF4G.mjs';
|
|
6
|
+
import '../chunk-JABLSOQN.mjs';
|
|
6
7
|
import * as React from 'react';
|
|
7
8
|
import { I18nProvider, useTranslation } from 'najm-i18n/react';
|
|
8
9
|
import { jsx } from 'react/jsx-runtime';
|
package/dist/adapters/next.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
2
|
import React__default from 'react';
|
|
3
|
-
import { N as NajmUIProviderProps } from '../NajmUIProvider-
|
|
4
|
-
import '../paginationLabels-
|
|
3
|
+
import { N as NajmUIProviderProps } from '../NajmUIProvider-BSbXaqak.js';
|
|
4
|
+
import '../paginationLabels-dZLSNxfo.js';
|
|
5
5
|
|
|
6
6
|
interface NextLinkAdapterProps extends Record<string, any> {
|
|
7
7
|
href: string;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { N as NajmTranslate, a as NTableCardPagination } from './paginationLabels-dZLSNxfo.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* How a list continues.
|
|
5
|
+
*
|
|
6
|
+
* - `paged` — numbered pages on desktop, scroll continuation on card viewports.
|
|
7
|
+
* - `infinite` — scroll continuation everywhere.
|
|
8
|
+
* - `all` — one request for the whole set, no controls. Only valid for a list
|
|
9
|
+
* with a proven bound.
|
|
10
|
+
*/
|
|
11
|
+
type ListStrategy = "all" | "infinite" | "paged";
|
|
12
|
+
/** The presentation mode resolved for the current viewport and strategy. */
|
|
13
|
+
type ResolvedListMode = "all" | "infinite" | "paged";
|
|
14
|
+
interface CardPaginationState {
|
|
15
|
+
/**
|
|
16
|
+
* The resolved mode. When omitted, `cardViewport` alone decides — a card
|
|
17
|
+
* viewport continues on scroll, a desktop one paginates.
|
|
18
|
+
*/
|
|
19
|
+
mode?: ResolvedListMode;
|
|
20
|
+
cardViewport?: boolean;
|
|
21
|
+
hasNextPage: boolean;
|
|
22
|
+
loadingMore: boolean;
|
|
23
|
+
/**
|
|
24
|
+
* Truthy when the last append failed. The value is not rendered — the message
|
|
25
|
+
* shown is `loadMoreError` from the labels, so a thrown `Error` or an API
|
|
26
|
+
* envelope can be passed straight through without leaking its text into the UI.
|
|
27
|
+
*/
|
|
28
|
+
loadMoreError?: unknown;
|
|
29
|
+
onLoadMore: () => unknown | Promise<unknown>;
|
|
30
|
+
}
|
|
31
|
+
interface CardPaginationLabels {
|
|
32
|
+
loadMoreError?: string;
|
|
33
|
+
retryLabel?: string;
|
|
34
|
+
itemsLoaded?: (count: number) => string;
|
|
35
|
+
}
|
|
36
|
+
declare const DEFAULT_CARD_PAGINATION_KEY_PREFIX = "common.pagination";
|
|
37
|
+
type DefaultCardPaginationPrefix = typeof DEFAULT_CARD_PAGINATION_KEY_PREFIX;
|
|
38
|
+
/** The three catalog keys `buildCardPaginationLabels` reads under `Prefix`. */
|
|
39
|
+
type CardPaginationKey<Prefix extends string = DefaultCardPaginationPrefix> = `${Prefix}.itemsLoaded` | `${Prefix}.loadMoreError` | `${Prefix}.retryLoadMore`;
|
|
40
|
+
/**
|
|
41
|
+
* Projects a translator onto the three card-continuation labels, matching
|
|
42
|
+
* `buildPaginationLabels` — same prefix convention, same key-per-field naming.
|
|
43
|
+
*
|
|
44
|
+
* The keys are named in the type, not just built at runtime, so an application
|
|
45
|
+
* whose `t` is typed to a generated union of its catalog can pass it directly
|
|
46
|
+
* and have the three keys verified against that union.
|
|
47
|
+
*/
|
|
48
|
+
declare function buildCardPaginationLabels<Prefix extends string = DefaultCardPaginationPrefix>(t: NajmTranslate<CardPaginationKey<Prefix>>, prefix?: Prefix): CardPaginationLabels;
|
|
49
|
+
/**
|
|
50
|
+
* Builds the `cardPagination` prop from the state a paged list already holds.
|
|
51
|
+
*
|
|
52
|
+
* The three modes are not interchangeable presentations of one thing: `paged`
|
|
53
|
+
* hands page-size control back to NTable, which measures the container and
|
|
54
|
+
* reports the size it wants through `onPaginationChange`; `all` renders exactly
|
|
55
|
+
* what it is given and shows no controls; `infinite` is the only one that needs
|
|
56
|
+
* continuation wiring, which is why the other two ignore the labels entirely.
|
|
57
|
+
*
|
|
58
|
+
* The second argument takes a translator as well as a label bundle. That is the
|
|
59
|
+
* common case — a list page has `t` in hand and nothing else to say about these
|
|
60
|
+
* three strings — and passing it here rather than pre-building labels also means
|
|
61
|
+
* the lookups only happen in `infinite` mode, where they are rendered.
|
|
62
|
+
*/
|
|
63
|
+
declare function createCardPagination<Prefix extends string = DefaultCardPaginationPrefix>(state: CardPaginationState, labels?: CardPaginationLabels | NajmTranslate<CardPaginationKey<Prefix>>, prefix?: Prefix): NTableCardPagination;
|
|
64
|
+
|
|
65
|
+
export { type CardPaginationKey as C, DEFAULT_CARD_PAGINATION_KEY_PREFIX as D, type ListStrategy as L, type ResolvedListMode as R, type CardPaginationLabels as a, type CardPaginationState as b, buildCardPaginationLabels as c, createCardPagination as d };
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// src/lib/pagination.ts
|
|
2
|
+
var DEFAULT_PAGE_SIZE = 25;
|
|
3
|
+
var DEFAULT_MAX_PAGE_SIZE = 100;
|
|
4
|
+
function toApiPage(result) {
|
|
5
|
+
return Array.isArray(result) ? { rows: result, total: null } : result;
|
|
6
|
+
}
|
|
7
|
+
function createOffsetPagination(pageIndex = 0, pageSize = DEFAULT_PAGE_SIZE, { maxLimit = DEFAULT_MAX_PAGE_SIZE } = {}) {
|
|
8
|
+
const safePageIndex = Math.max(0, Math.trunc(pageIndex));
|
|
9
|
+
const safePageSize = Math.min(maxLimit, Math.max(1, Math.trunc(pageSize)));
|
|
10
|
+
return { limit: safePageSize, offset: safePageIndex * safePageSize };
|
|
11
|
+
}
|
|
12
|
+
function getPageIndex({ limit, offset }) {
|
|
13
|
+
return Math.floor(Math.max(0, offset) / Math.max(1, limit));
|
|
14
|
+
}
|
|
15
|
+
async function fetchOffsetPage(fetchPage, pagination, { maxLimit = DEFAULT_MAX_PAGE_SIZE } = {}) {
|
|
16
|
+
const requestedLimit = Math.min(maxLimit, Math.max(1, pagination.limit));
|
|
17
|
+
const probeLimit = requestedLimit < maxLimit ? requestedLimit + 1 : requestedLimit;
|
|
18
|
+
const page = toApiPage(
|
|
19
|
+
await fetchPage({ limit: probeLimit, offset: pagination.offset })
|
|
20
|
+
);
|
|
21
|
+
const rows = page.rows.slice(0, requestedLimit);
|
|
22
|
+
const nextOffset = pagination.offset + rows.length;
|
|
23
|
+
if (page.total !== null) {
|
|
24
|
+
return {
|
|
25
|
+
rows,
|
|
26
|
+
hasNextPage: nextOffset < page.total,
|
|
27
|
+
nextOffset,
|
|
28
|
+
total: page.total
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
const hasNextPage = page.rows.length > requestedLimit || probeLimit === requestedLimit && page.rows.length === requestedLimit && toApiPage(await fetchPage({ limit: 1, offset: nextOffset })).rows.length > 0;
|
|
32
|
+
return { rows, hasNextPage, nextOffset, total: null };
|
|
33
|
+
}
|
|
34
|
+
function cleanQuery(query) {
|
|
35
|
+
return Object.fromEntries(
|
|
36
|
+
Object.entries(query).filter(
|
|
37
|
+
([, value]) => value !== void 0 && value !== null && value !== ""
|
|
38
|
+
)
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export { DEFAULT_MAX_PAGE_SIZE, DEFAULT_PAGE_SIZE, cleanQuery, createOffsetPagination, fetchOffsetPage, getPageIndex };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// src/format/format.ts
|
|
2
|
+
var DEFAULT_PLACEHOLDER = "\u2014";
|
|
3
|
+
var numberFormats = /* @__PURE__ */ new Map();
|
|
4
|
+
var dateFormats = /* @__PURE__ */ new Map();
|
|
5
|
+
function numberFormat(locale, options) {
|
|
6
|
+
const key = `${locale}|${options ? JSON.stringify(options) : ""}`;
|
|
7
|
+
let format = numberFormats.get(key);
|
|
8
|
+
if (!format) {
|
|
9
|
+
format = new Intl.NumberFormat(locale, options);
|
|
10
|
+
numberFormats.set(key, format);
|
|
11
|
+
}
|
|
12
|
+
return format;
|
|
13
|
+
}
|
|
14
|
+
function dateFormat(locale, options) {
|
|
15
|
+
const key = `${locale}|${JSON.stringify(options)}`;
|
|
16
|
+
let format = dateFormats.get(key);
|
|
17
|
+
if (!format) {
|
|
18
|
+
format = new Intl.DateTimeFormat(locale, options);
|
|
19
|
+
dateFormats.set(key, format);
|
|
20
|
+
}
|
|
21
|
+
return format;
|
|
22
|
+
}
|
|
23
|
+
function minorUnitScale(locale, currency) {
|
|
24
|
+
const digits = numberFormat(locale, { style: "currency", currency }).resolvedOptions().maximumFractionDigits ?? 2;
|
|
25
|
+
return 10 ** digits;
|
|
26
|
+
}
|
|
27
|
+
function toDate(value) {
|
|
28
|
+
const date = value instanceof Date ? value : new Date(value);
|
|
29
|
+
return Number.isNaN(date.getTime()) ? null : date;
|
|
30
|
+
}
|
|
31
|
+
function isBlank(value) {
|
|
32
|
+
return value === null || value === void 0 || value === "";
|
|
33
|
+
}
|
|
34
|
+
function formatCurrency(minorUnits, { locale, currency, placeholder = DEFAULT_PLACEHOLDER }) {
|
|
35
|
+
if (!currency) {
|
|
36
|
+
throw new Error(
|
|
37
|
+
"formatCurrency requires a `currency`. Set it on NajmFormatProvider or pass it here."
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
if (minorUnits === null || minorUnits === void 0) return placeholder;
|
|
41
|
+
if (!Number.isSafeInteger(minorUnits)) return placeholder;
|
|
42
|
+
return numberFormat(locale, {
|
|
43
|
+
style: "currency",
|
|
44
|
+
currency
|
|
45
|
+
}).format(minorUnits / minorUnitScale(locale, currency));
|
|
46
|
+
}
|
|
47
|
+
function formatNumber(value, { locale, placeholder = DEFAULT_PLACEHOLDER }, options) {
|
|
48
|
+
if (value === null || value === void 0 || !Number.isFinite(value)) {
|
|
49
|
+
return placeholder;
|
|
50
|
+
}
|
|
51
|
+
return numberFormat(locale, options).format(value);
|
|
52
|
+
}
|
|
53
|
+
function formatPercent(value, config, fractionDigits = 0) {
|
|
54
|
+
return formatNumber(value, config, {
|
|
55
|
+
style: "percent",
|
|
56
|
+
minimumFractionDigits: fractionDigits,
|
|
57
|
+
maximumFractionDigits: fractionDigits
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
function formatDate(value, {
|
|
61
|
+
locale,
|
|
62
|
+
timeZone,
|
|
63
|
+
placeholder = DEFAULT_PLACEHOLDER
|
|
64
|
+
}, options = { dateStyle: "medium" }) {
|
|
65
|
+
if (isBlank(value)) return placeholder;
|
|
66
|
+
const date = toDate(value);
|
|
67
|
+
if (!date) return placeholder;
|
|
68
|
+
return dateFormat(locale, { ...options, timeZone }).format(date);
|
|
69
|
+
}
|
|
70
|
+
function formatDateTime(value, config, options = {
|
|
71
|
+
dateStyle: "short",
|
|
72
|
+
timeStyle: "short"
|
|
73
|
+
}) {
|
|
74
|
+
return formatDate(value, config, options);
|
|
75
|
+
}
|
|
76
|
+
function formatTime(value, config) {
|
|
77
|
+
return formatDate(value, config, { timeStyle: "short" });
|
|
78
|
+
}
|
|
79
|
+
var RELATIVE_UNITS = [
|
|
80
|
+
["year", 31536e6],
|
|
81
|
+
["month", 2592e6],
|
|
82
|
+
["day", 864e5],
|
|
83
|
+
["hour", 36e5],
|
|
84
|
+
["minute", 6e4],
|
|
85
|
+
["second", 1e3]
|
|
86
|
+
];
|
|
87
|
+
function formatRelativeTime(value, { locale, placeholder = DEFAULT_PLACEHOLDER }, now = Date.now()) {
|
|
88
|
+
if (isBlank(value)) return placeholder;
|
|
89
|
+
const date = toDate(value);
|
|
90
|
+
if (!date) return placeholder;
|
|
91
|
+
const elapsed = date.getTime() - (now instanceof Date ? now.getTime() : now);
|
|
92
|
+
const format = new Intl.RelativeTimeFormat(locale, { numeric: "auto" });
|
|
93
|
+
for (const [unit, ms] of RELATIVE_UNITS) {
|
|
94
|
+
if (Math.abs(elapsed) >= ms) {
|
|
95
|
+
return format.format(Math.trunc(elapsed / ms), unit);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return format.format(0, "second");
|
|
99
|
+
}
|
|
100
|
+
function humanizeToken(value) {
|
|
101
|
+
return value.trim().replace(/[_-]+/g, " ").replace(/\s+/g, " ").replace(/\b\w/g, (letter) => letter.toUpperCase());
|
|
102
|
+
}
|
|
103
|
+
function localDateInput(date = /* @__PURE__ */ new Date()) {
|
|
104
|
+
const month = String(date.getMonth() + 1).padStart(2, "0");
|
|
105
|
+
const day = String(date.getDate()).padStart(2, "0");
|
|
106
|
+
return `${date.getFullYear()}-${month}-${day}`;
|
|
107
|
+
}
|
|
108
|
+
function slugify(value, { upperCase = false, maxLength = 160 } = {}) {
|
|
109
|
+
const normalized = value.normalize("NFD").replace(/[\u0300-\u036f]/g, "").trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, maxLength);
|
|
110
|
+
const slug = normalized || crypto.randomUUID().slice(0, 8);
|
|
111
|
+
return upperCase ? slug.toUpperCase() : slug;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export { DEFAULT_PLACEHOLDER, formatCurrency, formatDate, formatDateTime, formatNumber, formatPercent, formatRelativeTime, formatTime, humanizeToken, localDateInput, slugify };
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { useCallback, useSyncExternalStore } from 'react';
|
|
2
|
+
|
|
3
|
+
// src/hooks/useMediaQuery.ts
|
|
4
|
+
function useMediaQuery(query, serverSnapshot = false) {
|
|
5
|
+
const subscribe = useCallback(
|
|
6
|
+
(onStoreChange) => {
|
|
7
|
+
const media = window.matchMedia(query);
|
|
8
|
+
media.addEventListener("change", onStoreChange);
|
|
9
|
+
return () => media.removeEventListener("change", onStoreChange);
|
|
10
|
+
},
|
|
11
|
+
[query]
|
|
12
|
+
);
|
|
13
|
+
return useSyncExternalStore(
|
|
14
|
+
subscribe,
|
|
15
|
+
() => window.matchMedia(query).matches,
|
|
16
|
+
() => serverSnapshot
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
var DEFAULT_CARD_BREAKPOINT = 1024;
|
|
20
|
+
function useCardViewport(breakpoint = DEFAULT_CARD_BREAKPOINT) {
|
|
21
|
+
return useMediaQuery(`(max-width: ${breakpoint - 1}px)`);
|
|
22
|
+
}
|
|
23
|
+
function useDesktopTableMode(breakpoint = DEFAULT_CARD_BREAKPOINT) {
|
|
24
|
+
return useMediaQuery(`(min-width: ${breakpoint}px)`) ? "table" : "cards";
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// src/components/table/cardPagination.ts
|
|
28
|
+
var DEFAULT_CARD_PAGINATION_KEY_PREFIX = "common.pagination";
|
|
29
|
+
function buildCardPaginationLabels(t, prefix) {
|
|
30
|
+
const scope = prefix ?? DEFAULT_CARD_PAGINATION_KEY_PREFIX;
|
|
31
|
+
return {
|
|
32
|
+
loadMoreError: t(`${scope}.loadMoreError`),
|
|
33
|
+
retryLabel: t(`${scope}.retryLoadMore`),
|
|
34
|
+
itemsLoaded: (count) => t(`${scope}.itemsLoaded`, { count })
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
function createCardPagination(state, labels = {}, prefix) {
|
|
38
|
+
const mode = state.mode ?? (state.cardViewport ? "infinite" : "paged");
|
|
39
|
+
if (mode === "paged") return { mode: "paged" };
|
|
40
|
+
if (mode === "all") return { mode: "all" };
|
|
41
|
+
const resolved = typeof labels === "function" ? buildCardPaginationLabels(labels, prefix) : labels;
|
|
42
|
+
return {
|
|
43
|
+
mode: "infinite",
|
|
44
|
+
hasNextPage: state.hasNextPage,
|
|
45
|
+
loadingMore: state.loadingMore,
|
|
46
|
+
loadMoreError: state.loadMoreError ? resolved.loadMoreError : void 0,
|
|
47
|
+
onLoadMore: state.onLoadMore,
|
|
48
|
+
retryLabel: resolved.retryLabel,
|
|
49
|
+
loadMoreErrorLabel: resolved.loadMoreError,
|
|
50
|
+
itemsLoadedLabel: resolved.itemsLoaded
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export { DEFAULT_CARD_BREAKPOINT, DEFAULT_CARD_PAGINATION_KEY_PREFIX, buildCardPaginationLabels, createCardPagination, useCardViewport, useDesktopTableMode, useMediaQuery };
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useNajmPreferencesContext } from './chunk-USZUOJMK.mjs';
|
|
2
|
+
import { humanizeToken, DEFAULT_PLACEHOLDER, formatRelativeTime, formatTime, formatDateTime, formatDate, formatPercent, formatNumber, formatCurrency } from './chunk-JABLSOQN.mjs';
|
|
2
3
|
import * as React from 'react';
|
|
3
4
|
import { createContext, useContext, useMemo, useState, useCallback } from 'react';
|
|
4
5
|
import { jsx } from 'react/jsx-runtime';
|
|
@@ -62,109 +63,6 @@ function NBrandingStateProvider({
|
|
|
62
63
|
}
|
|
63
64
|
function noop() {
|
|
64
65
|
}
|
|
65
|
-
|
|
66
|
-
// src/format/format.ts
|
|
67
|
-
var DEFAULT_PLACEHOLDER = "\u2014";
|
|
68
|
-
var numberFormats = /* @__PURE__ */ new Map();
|
|
69
|
-
var dateFormats = /* @__PURE__ */ new Map();
|
|
70
|
-
function numberFormat(locale, options) {
|
|
71
|
-
const key = `${locale}|${options ? JSON.stringify(options) : ""}`;
|
|
72
|
-
let format = numberFormats.get(key);
|
|
73
|
-
if (!format) {
|
|
74
|
-
format = new Intl.NumberFormat(locale, options);
|
|
75
|
-
numberFormats.set(key, format);
|
|
76
|
-
}
|
|
77
|
-
return format;
|
|
78
|
-
}
|
|
79
|
-
function dateFormat(locale, options) {
|
|
80
|
-
const key = `${locale}|${JSON.stringify(options)}`;
|
|
81
|
-
let format = dateFormats.get(key);
|
|
82
|
-
if (!format) {
|
|
83
|
-
format = new Intl.DateTimeFormat(locale, options);
|
|
84
|
-
dateFormats.set(key, format);
|
|
85
|
-
}
|
|
86
|
-
return format;
|
|
87
|
-
}
|
|
88
|
-
function minorUnitScale(locale, currency) {
|
|
89
|
-
const digits = numberFormat(locale, { style: "currency", currency }).resolvedOptions().maximumFractionDigits ?? 2;
|
|
90
|
-
return 10 ** digits;
|
|
91
|
-
}
|
|
92
|
-
function toDate(value) {
|
|
93
|
-
const date = value instanceof Date ? value : new Date(value);
|
|
94
|
-
return Number.isNaN(date.getTime()) ? null : date;
|
|
95
|
-
}
|
|
96
|
-
function isBlank(value) {
|
|
97
|
-
return value === null || value === void 0 || value === "";
|
|
98
|
-
}
|
|
99
|
-
function formatCurrency(minorUnits, { locale, currency, placeholder = DEFAULT_PLACEHOLDER }) {
|
|
100
|
-
if (!currency) {
|
|
101
|
-
throw new Error(
|
|
102
|
-
"formatCurrency requires a `currency`. Set it on NajmFormatProvider or pass it here."
|
|
103
|
-
);
|
|
104
|
-
}
|
|
105
|
-
if (minorUnits === null || minorUnits === void 0) return placeholder;
|
|
106
|
-
if (!Number.isSafeInteger(minorUnits)) return placeholder;
|
|
107
|
-
return numberFormat(locale, {
|
|
108
|
-
style: "currency",
|
|
109
|
-
currency
|
|
110
|
-
}).format(minorUnits / minorUnitScale(locale, currency));
|
|
111
|
-
}
|
|
112
|
-
function formatNumber(value, { locale, placeholder = DEFAULT_PLACEHOLDER }, options) {
|
|
113
|
-
if (value === null || value === void 0 || !Number.isFinite(value)) {
|
|
114
|
-
return placeholder;
|
|
115
|
-
}
|
|
116
|
-
return numberFormat(locale, options).format(value);
|
|
117
|
-
}
|
|
118
|
-
function formatPercent(value, config, fractionDigits = 0) {
|
|
119
|
-
return formatNumber(value, config, {
|
|
120
|
-
style: "percent",
|
|
121
|
-
minimumFractionDigits: fractionDigits,
|
|
122
|
-
maximumFractionDigits: fractionDigits
|
|
123
|
-
});
|
|
124
|
-
}
|
|
125
|
-
function formatDate(value, {
|
|
126
|
-
locale,
|
|
127
|
-
timeZone,
|
|
128
|
-
placeholder = DEFAULT_PLACEHOLDER
|
|
129
|
-
}, options = { dateStyle: "medium" }) {
|
|
130
|
-
if (isBlank(value)) return placeholder;
|
|
131
|
-
const date = toDate(value);
|
|
132
|
-
if (!date) return placeholder;
|
|
133
|
-
return dateFormat(locale, { ...options, timeZone }).format(date);
|
|
134
|
-
}
|
|
135
|
-
function formatDateTime(value, config, options = {
|
|
136
|
-
dateStyle: "short",
|
|
137
|
-
timeStyle: "short"
|
|
138
|
-
}) {
|
|
139
|
-
return formatDate(value, config, options);
|
|
140
|
-
}
|
|
141
|
-
function formatTime(value, config) {
|
|
142
|
-
return formatDate(value, config, { timeStyle: "short" });
|
|
143
|
-
}
|
|
144
|
-
var RELATIVE_UNITS = [
|
|
145
|
-
["year", 31536e6],
|
|
146
|
-
["month", 2592e6],
|
|
147
|
-
["day", 864e5],
|
|
148
|
-
["hour", 36e5],
|
|
149
|
-
["minute", 6e4],
|
|
150
|
-
["second", 1e3]
|
|
151
|
-
];
|
|
152
|
-
function formatRelativeTime(value, { locale, placeholder = DEFAULT_PLACEHOLDER }, now = Date.now()) {
|
|
153
|
-
if (isBlank(value)) return placeholder;
|
|
154
|
-
const date = toDate(value);
|
|
155
|
-
if (!date) return placeholder;
|
|
156
|
-
const elapsed = date.getTime() - (now instanceof Date ? now.getTime() : now);
|
|
157
|
-
const format = new Intl.RelativeTimeFormat(locale, { numeric: "auto" });
|
|
158
|
-
for (const [unit, ms] of RELATIVE_UNITS) {
|
|
159
|
-
if (Math.abs(elapsed) >= ms) {
|
|
160
|
-
return format.format(Math.trunc(elapsed / ms), unit);
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
return format.format(0, "second");
|
|
164
|
-
}
|
|
165
|
-
function humanizeToken(value) {
|
|
166
|
-
return value.trim().replace(/[_-]+/g, " ").replace(/\s+/g, " ").replace(/\b\w/g, (letter) => letter.toUpperCase());
|
|
167
|
-
}
|
|
168
66
|
var NajmFormatContext = React.createContext(
|
|
169
67
|
null
|
|
170
68
|
);
|
|
@@ -215,4 +113,4 @@ function useNajmFormat() {
|
|
|
215
113
|
return value;
|
|
216
114
|
}
|
|
217
115
|
|
|
218
|
-
export {
|
|
116
|
+
export { NBrandingProvider, NBrandingStateProvider, NajmFormatProvider, normalizeBranding, useNBranding, useNBrandingEditor, useNajmFormat, useNajmFormatContext };
|
package/dist/format.d.ts
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Locale-aware value formatting.
|
|
3
|
+
*
|
|
4
|
+
* Pure functions with no React and no DOM: every input arrives through
|
|
5
|
+
* `NajmFormatConfig`. `NajmFormatProvider` binds them to the live locale and
|
|
6
|
+
* time zone, and is what an application normally uses — these are exported for
|
|
7
|
+
* the server, where there is no context to read.
|
|
8
|
+
*/
|
|
9
|
+
/** Rendered in place of a value that is absent or not formattable. */
|
|
10
|
+
declare const DEFAULT_PLACEHOLDER = "\u2014";
|
|
11
|
+
interface NajmFormatConfig {
|
|
12
|
+
/** BCP 47 tag handed to `Intl`, e.g. `"fr-MA"`. */
|
|
13
|
+
locale: string;
|
|
14
|
+
/**
|
|
15
|
+
* IANA zone every date is rendered in. Omitted means the host zone, which is
|
|
16
|
+
* the runtime's guess rather than the user's preference — supply it.
|
|
17
|
+
*/
|
|
18
|
+
timeZone?: string;
|
|
19
|
+
/** ISO 4217 code for `formatCurrency`, e.g. `"MAD"`. */
|
|
20
|
+
currency?: string;
|
|
21
|
+
/** Defaults to an em dash. */
|
|
22
|
+
placeholder?: string;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Formats an integer count of minor units — cents, centimes — as currency.
|
|
26
|
+
*
|
|
27
|
+
* Minor units are the input because money that survives a round trip is an
|
|
28
|
+
* integer; a major-unit float cannot represent 0.1 exactly and has no business
|
|
29
|
+
* in a ledger. A non-integer input is refused rather than rounded, since at
|
|
30
|
+
* this layer it means the caller has already lost precision upstream.
|
|
31
|
+
*/
|
|
32
|
+
declare function formatCurrency(minorUnits: number | null | undefined, { locale, currency, placeholder }: NajmFormatConfig): string;
|
|
33
|
+
declare function formatNumber(value: number | null | undefined, { locale, placeholder }: NajmFormatConfig, options?: Intl.NumberFormatOptions): string;
|
|
34
|
+
declare function formatPercent(value: number | null | undefined, config: NajmFormatConfig, fractionDigits?: number): string;
|
|
35
|
+
declare function formatDate(value: Date | number | string | null | undefined, { locale, timeZone, placeholder, }: NajmFormatConfig, options?: Intl.DateTimeFormatOptions): string;
|
|
36
|
+
declare function formatDateTime(value: Date | number | string | null | undefined, config: NajmFormatConfig, options?: Intl.DateTimeFormatOptions): string;
|
|
37
|
+
declare function formatTime(value: Date | number | string | null | undefined, config: NajmFormatConfig): string;
|
|
38
|
+
/**
|
|
39
|
+
* Renders a timestamp as distance from now — "3 days ago", "in 2 hours".
|
|
40
|
+
*
|
|
41
|
+
* Time zone is not a parameter: an elapsed duration is the same in every zone.
|
|
42
|
+
*/
|
|
43
|
+
declare function formatRelativeTime(value: Date | number | string | null | undefined, { locale, placeholder }: NajmFormatConfig, now?: Date | number): string;
|
|
44
|
+
/**
|
|
45
|
+
* Turns a machine token into readable text: `out_for_delivery` → `Out For
|
|
46
|
+
* Delivery`.
|
|
47
|
+
*
|
|
48
|
+
* The fallback for a value with no catalog entry, not a substitute for one.
|
|
49
|
+
* Anything user-visible and known in advance belongs in the translations.
|
|
50
|
+
*/
|
|
51
|
+
declare function humanizeToken(value: string): string;
|
|
52
|
+
/**
|
|
53
|
+
* Today as `YYYY-MM-DD` in the host's zone, for `<input type="date">`.
|
|
54
|
+
*
|
|
55
|
+
* Not `toISOString().slice(0, 10)`, which is the same line everyone writes and
|
|
56
|
+
* is wrong west of UTC for the first hours of the day: it converts to UTC
|
|
57
|
+
* first, so a date picker in Casablanca opens on tomorrow. The parts are read
|
|
58
|
+
* off the local calendar instead.
|
|
59
|
+
*
|
|
60
|
+
* Deliberately host-zone rather than preference-zone. This produces the value a
|
|
61
|
+
* date *input* round-trips, and that control is bound to the machine the user
|
|
62
|
+
* is typing on; `formatDate` is what renders a date for reading.
|
|
63
|
+
*/
|
|
64
|
+
declare function localDateInput(date?: Date): string;
|
|
65
|
+
interface SlugifyOptions {
|
|
66
|
+
/** Uppercases the result, for an identifier conventionally read in caps. */
|
|
67
|
+
upperCase?: boolean;
|
|
68
|
+
/** Longest slug produced, before the case change. Defaults to 160. */
|
|
69
|
+
maxLength?: number;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Turns a label into a URL- and identifier-safe token: `Épicerie Fine` →
|
|
73
|
+
* `epicerie-fine`.
|
|
74
|
+
*
|
|
75
|
+
* Accents are folded rather than dropped. Stripping them outright is the usual
|
|
76
|
+
* one-liner and it silently eats letters — `Épicerie` becomes `picerie` — which
|
|
77
|
+
* is a poor slug in exactly the languages most likely to need one.
|
|
78
|
+
*
|
|
79
|
+
* A value with nothing to transliterate — Arabic or CJK, where folding has no
|
|
80
|
+
* ASCII to fall back to — yields a random token rather than an empty string.
|
|
81
|
+
* Empty is never a usable slug, and returning one pushes the failure to a
|
|
82
|
+
* uniqueness constraint far from the cause.
|
|
83
|
+
*/
|
|
84
|
+
declare function slugify(value: string, { upperCase, maxLength }?: SlugifyOptions): string;
|
|
85
|
+
|
|
86
|
+
export { DEFAULT_PLACEHOLDER, type NajmFormatConfig, type SlugifyOptions, formatCurrency, formatDate, formatDateTime, formatNumber, formatPercent, formatRelativeTime, formatTime, humanizeToken, localDateInput, slugify };
|
package/dist/format.mjs
ADDED