najm-kit 2.7.3 → 2.8.1

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 CHANGED
@@ -1,6 +1,32 @@
1
- # Changelog
2
-
3
- ## 2.6.2
1
+ # Changelog
2
+
3
+ ## 2.8.1 - 2026-08-08
4
+
5
+ - Added schema-driven form development tools to `NajmAppProvider`. Passing
6
+ `formDevTools` enables F8 filling for every `NForm` and `WizardForm` without
7
+ an application helper or additional provider.
8
+ - Added built-in Zod 4 form-value generation with per-form overrides for
9
+ relation fields and other application-owned values. Existing explicit
10
+ `devTools.enabled` and `devTools.fill` usage remains supported.
11
+
12
+ ## 2.8.0 - 2026-08-08
13
+
14
+ - Added server-safe `najm-kit/format` helpers for currency minor units,
15
+ numbers, percentages, dates, times, relative time, tokens, local date inputs,
16
+ and slugs. Client applications can use the same contract reactively through
17
+ `NajmFormatProvider` and `useNajmFormat`.
18
+ - Added the server-safe `najm-kit/pagination` offset protocol, including bounded
19
+ page creation, total-aware continuation, probe-row continuation for APIs
20
+ without totals, and query cleanup that preserves meaningful `false` and `0`.
21
+ - Added the optional-peer `najm-kit/query` entry with offset infinite-query and
22
+ responsive paged/card-list hooks, plus shared card-pagination adapters and
23
+ localized continuation labels.
24
+ - Extended `NajmAppProvider` with formatting locale, currency, and placeholder
25
+ configuration so applications can bind language, time zone, and formatting
26
+ without another host bridge provider.
27
+ - Added shared media-query/card-viewport helpers and avatar-source utilities.
28
+
29
+ ## 2.6.2
4
30
 
5
31
  - 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
32
  - `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
@@ -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,7 +145,42 @@ 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 |
148
+ | Overlays | Command palette, Tooltip, Toast |
149
+
150
+ ## Global form development tools
151
+
152
+ Enable schema-driven test values once on the full application provider. Every
153
+ `NForm` and `WizardForm` below it then fills from its Zod schema when F8 is
154
+ pressed; applications do not need a second provider or a form-fill helper.
155
+
156
+ ```tsx
157
+ import { NajmAppProvider } from "najm-kit/app";
158
+
159
+ <NajmAppProvider formDevTools>
160
+ <App />
161
+ </NajmAppProvider>;
162
+ ```
163
+
164
+ Pass a boolean to control it from application settings:
165
+
166
+ ```tsx
167
+ <NajmAppProvider formDevTools={formFillEnabled}>
168
+ <App />
169
+ </NajmAppProvider>
170
+ ```
171
+
172
+ Forms with live relation options can override only those fields. The provider
173
+ still owns enablement and Najm Kit still owns schema traversal and generation.
174
+
175
+ ```tsx
176
+ <NForm
177
+ schema={orderSchema}
178
+ devTools={{ overrides: { customerId: customerOptions } }}
179
+ onSubmit={saveOrder}
180
+ >
181
+ {/* fields */}
182
+ </NForm>
183
+ ```
149
184
 
150
185
  ## ImageInput and AvatarInput
151
186
 
@@ -212,10 +247,93 @@ Key behaviors:
212
247
  a newer value, and object URLs created by the component are tracked so
213
248
  consumer-owned blob URLs are never revoked.
214
249
 
215
- `AvatarInput` forwards every preview and accessibility prop unchanged while
216
- preserving its circular, size, fill, and camera-icon defaults.
217
-
218
- ## Hooks
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
219
337
 
220
338
  ```tsx
221
339
  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 { b as NTablePaginationLabels, N as NajmTranslate } from './paginationLabels-DgHutNWz.js';
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';
@@ -1,10 +1,11 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
2
  import { Translations } from 'najm-i18n';
3
- import { N as NBrandingInput } from '../NBrandingContext-dkADu8JS.js';
3
+ import { F as FormDevToolsOptions, N as NBrandingInput } from '../formFill-BcH-m9Kf.js';
4
4
  import { NajmNextUIProviderProps } from './next.js';
5
5
  import 'react';
6
- import '../NajmUIProvider-IFU3dFkn.js';
7
- import '../paginationLabels-DgHutNWz.js';
6
+ import 'zod';
7
+ import '../NajmUIProvider-BSbXaqak.js';
8
+ import '../paginationLabels-dZLSNxfo.js';
8
9
 
9
10
  /** Branding shown by the kit's chrome. Purely presentational values. */
10
11
  interface NajmAppBranding {
@@ -13,6 +14,11 @@ interface NajmAppBranding {
13
14
  logoCollapsed?: string | null;
14
15
  }
15
16
  interface NajmAppProviderProps extends Omit<NajmNextUIProviderProps, 't'> {
17
+ /**
18
+ * Enables schema-driven form filling for every `NForm` and `WizardForm`.
19
+ * `true` uses F8; an options object can choose another shortcut.
20
+ */
21
+ formDevTools?: boolean | FormDevToolsOptions;
16
22
  /**
17
23
  * Catalog for `najm-i18n`. Supplying it mounts an `I18nProvider` and derives
18
24
  * the pagination labels from it, so `t` is not a prop here — the provider
@@ -99,6 +105,6 @@ interface NajmAppProviderProps extends Omit<NajmNextUIProviderProps, 't'> {
99
105
  * state machine of its own above this provider. The controlled `design` and
100
106
  * `branding` props still work for applications that already do.
101
107
  */
102
- declare function NajmAppProvider({ translations, initialLanguage, defaultLanguage, languageEndpoint, appName, initialBranding, ...props }: NajmAppProviderProps): react_jsx_runtime.JSX.Element;
108
+ declare function NajmAppProvider({ translations, initialLanguage, defaultLanguage, languageEndpoint, appName, initialBranding, formDevTools, ...props }: NajmAppProviderProps): react_jsx_runtime.JSX.Element;
103
109
 
104
110
  export { type NajmAppBranding, NajmAppProvider, type NajmAppProviderProps };
@@ -1,9 +1,9 @@
1
1
  'use client';
2
- import { NBrandingStateProvider, NajmFormatProvider } from '../chunk-VKQIRB7F.mjs';
2
+ import { FormDevToolsProvider, NBrandingStateProvider, NajmFormatProvider } from '../chunk-GHMR45H3.mjs';
3
3
  import { NajmNextUIProvider } from '../chunk-IRFFSAO2.mjs';
4
4
  import '../chunk-USZUOJMK.mjs';
5
5
  import '../chunk-KVZACF4G.mjs';
6
- import '../chunk-GPHWBOSP.mjs';
6
+ import '../chunk-JABLSOQN.mjs';
7
7
  import * as React from 'react';
8
8
  import { I18nProvider, useTranslation } from 'najm-i18n/react';
9
9
  import { jsx } from 'react/jsx-runtime';
@@ -72,6 +72,7 @@ function NajmAppProvider({
72
72
  languageEndpoint = DEFAULT_LANGUAGE_ENDPOINT,
73
73
  appName,
74
74
  initialBranding,
75
+ formDevTools,
75
76
  ...props
76
77
  }) {
77
78
  const persistLanguage = React.useCallback(
@@ -91,8 +92,10 @@ function NajmAppProvider({
91
92
  [languageEndpoint]
92
93
  );
93
94
  const seeded = appName ? { appName, ...initialBranding } : initialBranding;
94
- if (!translations) return /* @__PURE__ */ jsx(NajmAppNoI18n, { ...props, initialBranding: seeded });
95
- return /* @__PURE__ */ jsx(
95
+ if (!translations) {
96
+ return /* @__PURE__ */ jsx(FormDevToolsProvider, { value: formDevTools, children: /* @__PURE__ */ jsx(NajmAppNoI18n, { ...props, initialBranding: seeded }) });
97
+ }
98
+ return /* @__PURE__ */ jsx(FormDevToolsProvider, { value: formDevTools, children: /* @__PURE__ */ jsx(
96
99
  I18nProvider,
97
100
  {
98
101
  translations,
@@ -101,7 +104,7 @@ function NajmAppProvider({
101
104
  onLanguageChange: persistLanguage,
102
105
  children: /* @__PURE__ */ jsx(NajmAppUI, { ...props, initialBranding: seeded })
103
106
  }
104
- );
107
+ ) });
105
108
  }
106
109
 
107
110
  export { NajmAppProvider };
@@ -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-IFU3dFkn.js';
4
- import '../paginationLabels-DgHutNWz.js';
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;
@@ -1,4 +1,4 @@
1
- import { N as NajmTranslate, a as NTableCardPagination } from './paginationLabels-DgHutNWz.js';
1
+ import { N as NajmTranslate, a as NTableCardPagination } from './paginationLabels-dZLSNxfo.js';
2
2
 
3
3
  /**
4
4
  * How a list continues.
@@ -34,11 +34,18 @@ interface CardPaginationLabels {
34
34
  itemsLoaded?: (count: number) => string;
35
35
  }
36
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`;
37
40
  /**
38
41
  * Projects a translator onto the three card-continuation labels, matching
39
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.
40
47
  */
41
- declare function buildCardPaginationLabels(t: NajmTranslate, prefix?: string): CardPaginationLabels;
48
+ declare function buildCardPaginationLabels<Prefix extends string = DefaultCardPaginationPrefix>(t: NajmTranslate<CardPaginationKey<Prefix>>, prefix?: Prefix): CardPaginationLabels;
42
49
  /**
43
50
  * Builds the `cardPagination` prop from the state a paged list already holds.
44
51
  *
@@ -47,7 +54,12 @@ declare function buildCardPaginationLabels(t: NajmTranslate, prefix?: string): C
47
54
  * reports the size it wants through `onPaginationChange`; `all` renders exactly
48
55
  * what it is given and shows no controls; `infinite` is the only one that needs
49
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.
50
62
  */
51
- declare function createCardPagination(state: CardPaginationState, labels?: CardPaginationLabels): NTableCardPagination;
63
+ declare function createCardPagination<Prefix extends string = DefaultCardPaginationPrefix>(state: CardPaginationState, labels?: CardPaginationLabels | NajmTranslate<CardPaginationKey<Prefix>>, prefix?: Prefix): NTableCardPagination;
52
64
 
53
- export { type CardPaginationLabels as C, DEFAULT_CARD_PAGINATION_KEY_PREFIX as D, type ListStrategy as L, type ResolvedListMode as R, type CardPaginationState as a, buildCardPaginationLabels as b, createCardPagination as c };
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,333 @@
1
+ import { useNajmPreferencesContext } from './chunk-USZUOJMK.mjs';
2
+ import { humanizeToken, DEFAULT_PLACEHOLDER, formatRelativeTime, formatTime, formatDateTime, formatDate, formatPercent, formatNumber, formatCurrency } from './chunk-JABLSOQN.mjs';
3
+ import * as React2 from 'react';
4
+ import { createContext, useContext, useMemo, useState, useCallback } from 'react';
5
+ import { jsx } from 'react/jsx-runtime';
6
+
7
+ function normalizeBranding(input) {
8
+ if (!input) return {};
9
+ const value = {};
10
+ const expanded = input.logoExpanded ?? input.sidebarLogoExpandedPath;
11
+ const collapsed = input.logoCollapsed ?? input.sidebarLogoCollapsedPath;
12
+ if (input.appName !== void 0) value.appName = input.appName;
13
+ if (input.logoFallback !== void 0) value.logoFallback = input.logoFallback;
14
+ if (input.logoHref !== void 0) value.logoHref = input.logoHref;
15
+ if (expanded !== void 0) value.logoExpanded = expanded;
16
+ if (collapsed !== void 0) value.logoCollapsed = collapsed;
17
+ return value;
18
+ }
19
+ var NBrandingContext = createContext(null);
20
+ function useNBranding() {
21
+ return useContext(NBrandingContext);
22
+ }
23
+ function NBrandingProvider({
24
+ children,
25
+ appName,
26
+ logoExpanded,
27
+ logoCollapsed,
28
+ logoFallback,
29
+ logoHref
30
+ }) {
31
+ const value = useMemo(
32
+ () => ({ appName, logoExpanded, logoCollapsed, logoFallback, logoHref }),
33
+ [appName, logoExpanded, logoCollapsed, logoFallback, logoHref]
34
+ );
35
+ return /* @__PURE__ */ jsx(NBrandingContext.Provider, { value, children });
36
+ }
37
+ var NBrandingEditorContext = createContext(null);
38
+ function useNBrandingEditor() {
39
+ return useContext(NBrandingEditorContext);
40
+ }
41
+ function NBrandingStateProvider({
42
+ children,
43
+ branding,
44
+ initialBranding
45
+ }) {
46
+ const [state, setState] = useState(
47
+ () => normalizeBranding(initialBranding ?? branding)
48
+ );
49
+ const setBranding = useCallback((patch) => {
50
+ const marks = normalizeBranding(patch);
51
+ setState((current) => ({ ...current, ...marks }));
52
+ }, []);
53
+ const controlled = useMemo(
54
+ () => branding ? normalizeBranding(branding) : void 0,
55
+ [branding]
56
+ );
57
+ const resolved = controlled ?? state;
58
+ const editor = useMemo(
59
+ () => ({ branding: resolved, setBranding: branding ? noop : setBranding }),
60
+ [resolved, branding, setBranding]
61
+ );
62
+ return /* @__PURE__ */ jsx(NBrandingEditorContext.Provider, { value: editor, children: /* @__PURE__ */ jsx(NBrandingProvider, { ...resolved, children }) });
63
+ }
64
+ function noop() {
65
+ }
66
+
67
+ // src/components/form/formFill.ts
68
+ var CITIES = ["Casablanca", "Rabat", "Marrakesh", "Tangier"];
69
+ var SCHOOL_LEVELS = ["Primary", "Middle school", "Secondary school"];
70
+ var CLOTHING_SIZES = ["6 years", "8 years", "10 years", "12 years"];
71
+ function pick(values) {
72
+ if (values.length === 0) return void 0;
73
+ return values[Math.floor(Math.random() * values.length)];
74
+ }
75
+ function randomDigits(length) {
76
+ return Array.from({ length }, () => Math.floor(Math.random() * 10)).join("");
77
+ }
78
+ function randomToken(length = 8) {
79
+ const alphabet = "abcdefghijklmnopqrstuvwxyz0123456789";
80
+ return Array.from(
81
+ { length },
82
+ () => alphabet[Math.floor(Math.random() * alphabet.length)]
83
+ ).join("");
84
+ }
85
+ function asZodLike(schema) {
86
+ return schema && typeof schema === "object" ? schema : void 0;
87
+ }
88
+ function definition(schema) {
89
+ const current = asZodLike(schema);
90
+ return current?._def ?? current?.def ?? {};
91
+ }
92
+ function schemaKind(schema) {
93
+ const currentDefinition = definition(schema);
94
+ const raw = currentDefinition.typeName ?? currentDefinition.type;
95
+ if (typeof raw !== "string") return "";
96
+ return raw.replace(/^Zod/, "").toLowerCase();
97
+ }
98
+ function unwrap(schema) {
99
+ const seen = /* @__PURE__ */ new Set();
100
+ let current = asZodLike(schema);
101
+ while (current && !seen.has(current)) {
102
+ seen.add(current);
103
+ const kind = schemaKind(current);
104
+ const currentDefinition = definition(current);
105
+ const inner = currentDefinition.innerType ?? currentDefinition.schema ?? (kind === "pipe" ? currentDefinition.out ?? currentDefinition.in : void 0);
106
+ if (!inner || typeof inner !== "object") break;
107
+ current = inner;
108
+ }
109
+ return current;
110
+ }
111
+ function objectShape(schema) {
112
+ const current = unwrap(schema);
113
+ if (schemaKind(current) !== "object") return null;
114
+ const shape = current?.shape ?? definition(current).shape;
115
+ return typeof shape === "function" ? shape() : shape ?? null;
116
+ }
117
+ function arrayElement(schema) {
118
+ const current = unwrap(schema);
119
+ if (schemaKind(current) !== "array") return null;
120
+ const currentDefinition = definition(current);
121
+ const element = current?.element ?? currentDefinition.element ?? currentDefinition.type;
122
+ return element && typeof element === "object" ? element : null;
123
+ }
124
+ function enumValues(schema) {
125
+ const current = unwrap(schema);
126
+ const currentDefinition = definition(current);
127
+ const kind = schemaKind(current);
128
+ if (kind === "literal") {
129
+ const values2 = currentDefinition.values;
130
+ if (Array.isArray(values2)) return values2;
131
+ return "value" in currentDefinition ? [currentDefinition.value] : [];
132
+ }
133
+ if (kind !== "enum" && kind !== "nativeenum") return [];
134
+ const entries = currentDefinition.entries;
135
+ if (entries && typeof entries === "object") return Object.values(entries);
136
+ const values = currentDefinition.values;
137
+ if (Array.isArray(values)) return values;
138
+ return Array.isArray(current?.options) ? [...current.options] : [];
139
+ }
140
+ function stringFormat(schema) {
141
+ const currentDefinition = definition(unwrap(schema));
142
+ if (typeof currentDefinition.format === "string") return currentDefinition.format;
143
+ const checks = Array.isArray(currentDefinition.checks) ? currentDefinition.checks : [];
144
+ for (const check of checks) {
145
+ if (!check || typeof check !== "object") continue;
146
+ const value = check;
147
+ const format = value.format ?? value.kind;
148
+ if (typeof format === "string") return format;
149
+ }
150
+ return "";
151
+ }
152
+ function resolveOverride(override, fieldName) {
153
+ if (typeof override === "function") return override(fieldName);
154
+ if (Array.isArray(override)) {
155
+ const value = pick(override);
156
+ if (value && typeof value === "object" && "value" in value) {
157
+ return value.value;
158
+ }
159
+ return value;
160
+ }
161
+ return override;
162
+ }
163
+ function localDateInput(date = /* @__PURE__ */ new Date()) {
164
+ return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")}`;
165
+ }
166
+ function birthDate(child) {
167
+ const date = /* @__PURE__ */ new Date();
168
+ date.setFullYear(date.getFullYear() - (child ? 10 : 35));
169
+ return localDateInput(date);
170
+ }
171
+ function fieldValue(fieldName, schema, siblingFields) {
172
+ const key = fieldName.toLowerCase();
173
+ const current = unwrap(schema);
174
+ const kind = schemaKind(current);
175
+ const format = stringFormat(current);
176
+ const values = enumValues(current);
177
+ if (values.length) return pick(values);
178
+ if (kind === "boolean") return true;
179
+ if (kind === "number" || kind === "bigint") {
180
+ if (key.includes("sortorder")) return 1;
181
+ if (key.includes("quantity")) return 5;
182
+ return 10;
183
+ }
184
+ if (format === "uuid" || key.endsWith("uuid")) {
185
+ return "9cc2c93f-f545-4e07-9f77-f79f08a71dd5";
186
+ }
187
+ if (format === "email" || key.includes("email")) {
188
+ return `test-${randomToken()}@example.com`;
189
+ }
190
+ if (format === "url" || key === "image" || key.includes("imageurl") || key.endsWith("url")) {
191
+ return `https://picsum.photos/seed/${randomToken()}/800/600`;
192
+ }
193
+ if (key === "month") return `${localDateInput().slice(0, 7)}-01`;
194
+ if (key === "registrationdate") return localDateInput();
195
+ if (format === "date" || key.includes("dateofbirth")) {
196
+ const child = siblingFields.has("schoolLevel") || siblingFields.has("clothingSize");
197
+ return birthDate(child);
198
+ }
199
+ if (key.endsWith("id") || key.endsWith("by")) return "";
200
+ if (key === "name") {
201
+ if (siblingFields.has("sku")) return `Test product ${randomToken(4)}`;
202
+ if (siblingFields.has("slug")) return `Test category ${randomToken(4)}`;
203
+ return "Test User";
204
+ }
205
+ if (key.includes("legalname")) return "Test User";
206
+ if (key.includes("phone")) return `+2126${randomDigits(8)}`;
207
+ if (key.endsWith("cin")) return `AB${randomDigits(6)}`;
208
+ if (key.includes("address")) return `10 Test Street, ${pick(CITIES)}`;
209
+ if (key.includes("schoollevel")) return pick(SCHOOL_LEVELS);
210
+ if (key.includes("clothingsize")) return pick(CLOTHING_SIZES);
211
+ if (key.includes("shoesize")) return "36";
212
+ if (key.includes("relationship")) return "Legal guardian";
213
+ if (key.includes("activationtargetmad")) return "7500";
214
+ if (key === "slug") return `test-${randomToken(6)}`;
215
+ if (key === "sku") return `TEST-${randomToken(8).toUpperCase()}`;
216
+ if (key.includes("amountmad") || key.includes("pricemad") || key.includes("limitmad") || key.includes("targetmad")) {
217
+ return "100.00";
218
+ }
219
+ if (key.includes("reason")) return "Generated for form testing.";
220
+ if (key.includes("notes")) return "Generated form testing note.";
221
+ if (key.includes("description")) return "Generated description for form testing.";
222
+ if (key.includes("code")) return randomToken(8).toUpperCase();
223
+ return `Test ${fieldName}`;
224
+ }
225
+ function buildFormFill(schema, overrides = {}) {
226
+ const shape = objectShape(schema) ?? {};
227
+ const siblingFields = new Set(Object.keys(shape));
228
+ const output = {};
229
+ for (const [fieldName, fieldSchema] of Object.entries(shape)) {
230
+ if (Object.prototype.hasOwnProperty.call(overrides, fieldName)) {
231
+ output[fieldName] = resolveOverride(overrides[fieldName], fieldName);
232
+ continue;
233
+ }
234
+ const nestedShape = objectShape(fieldSchema);
235
+ if (nestedShape) {
236
+ output[fieldName] = buildFormFill(fieldSchema);
237
+ continue;
238
+ }
239
+ const element = arrayElement(fieldSchema);
240
+ if (element) {
241
+ output[fieldName] = objectShape(element) ? [buildFormFill(element)] : [fieldValue(fieldName, element, siblingFields)];
242
+ continue;
243
+ }
244
+ output[fieldName] = fieldValue(fieldName, fieldSchema, siblingFields);
245
+ }
246
+ return output;
247
+ }
248
+ var DEFAULT_FORM_DEV_TOOLS = {
249
+ enabled: false,
250
+ shortcut: "F8"
251
+ };
252
+ var FormDevToolsContext = createContext(
253
+ DEFAULT_FORM_DEV_TOOLS
254
+ );
255
+ function normalizeFormDevTools(value) {
256
+ if (typeof value === "boolean") {
257
+ return { ...DEFAULT_FORM_DEV_TOOLS, enabled: value };
258
+ }
259
+ if (!value) return DEFAULT_FORM_DEV_TOOLS;
260
+ return {
261
+ enabled: value.enabled ?? true,
262
+ shortcut: value.shortcut ?? DEFAULT_FORM_DEV_TOOLS.shortcut
263
+ };
264
+ }
265
+ function FormDevToolsProvider({
266
+ children,
267
+ value
268
+ }) {
269
+ const resolved = useMemo(
270
+ () => normalizeFormDevTools(value),
271
+ [value]
272
+ );
273
+ return /* @__PURE__ */ jsx(FormDevToolsContext.Provider, { value: resolved, children });
274
+ }
275
+ function useResolvedFormDevTools(schema, local) {
276
+ const global = useContext(FormDevToolsContext);
277
+ const options = typeof local === "object" ? local : void 0;
278
+ const enabled = typeof local === "boolean" ? local : options?.enabled ?? global.enabled;
279
+ const shortcut = options?.shortcut ?? global.shortcut;
280
+ const fill = options?.fill ?? (schema ? () => buildFormFill(schema, options?.overrides) : void 0);
281
+ return { enabled, shortcut, fill };
282
+ }
283
+ var NajmFormatContext = React2.createContext(
284
+ null
285
+ );
286
+ function NajmFormatProvider({
287
+ children,
288
+ locale,
289
+ currency,
290
+ timeZone,
291
+ placeholder = DEFAULT_PLACEHOLDER
292
+ }) {
293
+ const preferences = useNajmPreferencesContext();
294
+ const resolvedTimeZone = timeZone ?? preferences?.timeZone ?? Intl.DateTimeFormat().resolvedOptions().timeZone;
295
+ const value = React2.useMemo(() => {
296
+ const config = {
297
+ locale,
298
+ timeZone: resolvedTimeZone,
299
+ currency,
300
+ placeholder
301
+ };
302
+ return {
303
+ locale,
304
+ timeZone: resolvedTimeZone,
305
+ currency,
306
+ placeholder,
307
+ config,
308
+ money: (minorUnits) => formatCurrency(minorUnits, config),
309
+ number: (value_, options) => formatNumber(value_, config, options),
310
+ percent: (value_, digits) => formatPercent(value_, config, digits),
311
+ date: (value_, options) => formatDate(value_, config, options),
312
+ dateTime: (value_) => formatDateTime(value_, config),
313
+ time: (value_) => formatTime(value_, config),
314
+ relativeTime: (value_) => formatRelativeTime(value_, config),
315
+ humanize: humanizeToken
316
+ };
317
+ }, [locale, resolvedTimeZone, currency, placeholder]);
318
+ return /* @__PURE__ */ jsx(NajmFormatContext.Provider, { value, children });
319
+ }
320
+ function useNajmFormatContext() {
321
+ return React2.useContext(NajmFormatContext);
322
+ }
323
+ function useNajmFormat() {
324
+ const value = React2.useContext(NajmFormatContext);
325
+ if (!value) {
326
+ throw new Error(
327
+ "useNajmFormat must be rendered under a NajmFormatProvider or NajmAppProvider."
328
+ );
329
+ }
330
+ return value;
331
+ }
332
+
333
+ export { FormDevToolsProvider, NBrandingProvider, NBrandingStateProvider, NajmFormatProvider, buildFormFill, normalizeBranding, useNBranding, useNBrandingEditor, useNajmFormat, useNajmFormatContext, useResolvedFormDevTools };
@@ -100,5 +100,15 @@ function formatRelativeTime(value, { locale, placeholder = DEFAULT_PLACEHOLDER }
100
100
  function humanizeToken(value) {
101
101
  return value.trim().replace(/[_-]+/g, " ").replace(/\s+/g, " ").replace(/\b\w/g, (letter) => letter.toUpperCase());
102
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
+ }
103
113
 
104
- export { DEFAULT_PLACEHOLDER, formatCurrency, formatDate, formatDateTime, formatNumber, formatPercent, formatRelativeTime, formatTime, humanizeToken };
114
+ export { DEFAULT_PLACEHOLDER, formatCurrency, formatDate, formatDateTime, formatNumber, formatPercent, formatRelativeTime, formatTime, humanizeToken, localDateInput, slugify };