@verma-consulting/design-library 0.1.47 → 0.1.49

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/README.md CHANGED
@@ -2,6 +2,10 @@
2
2
 
3
3
  Shared MUI-based React components for Verma Consulting applications. Use this package for consistent UI patterns, forms, and design tokens across projects.
4
4
 
5
+ ## Requirements
6
+
7
+ - **Node.js** `>=24.4.1` (see `engines` in `package.json`). For local development, use **24.4.1** via `.nvmrc`, `.node-version`, `.mise.toml`, or Volta (`volta` field in `package.json`).
8
+
5
9
  ## Installation
6
10
 
7
11
  ```bash
@@ -60,6 +64,7 @@ The library re-exports **all of `@mui/material`**, so you can use MUI components
60
64
  | [FormSnackBar](./docs/FormSnackBar.md) | Snackbar for form success/error messages. |
61
65
  | [SearchableSelect](./docs/SearchableSelect.md) | Searchable single/multi select with view/edit modes. |
62
66
  | [PhoneNumberField](./docs/PhoneNumberField.md) | Phone input with country code and E.164 output. |
67
+ | [OTPField](./docs/OTPField.md) | One-time code input (single-character cells, paste, mask). |
63
68
  | [InputFileUpload](./docs/InputFileUpload.md) | Button that opens a file picker. |
64
69
  | [ImageUploadAvatar](./docs/ImageUploadAvatar.md) | Avatar-style image upload with preview and clear. |
65
70
  | [TabPanel](./docs/TabPanel.md) | Panel content for MUI Tabs (by index). |
package/dist/index.d.mts CHANGED
@@ -2,7 +2,7 @@ import { DialogProps, DrawerProps } from '@mui/material';
2
2
  export * from '@mui/material';
3
3
  import * as react_jsx_runtime from 'react/jsx-runtime';
4
4
  import * as React from 'react';
5
- import React__default, { FC, KeyboardEvent } from 'react';
5
+ import React__default, { FC, KeyboardEvent, CSSProperties } from 'react';
6
6
  import MuiTablePagination, { LabelDisplayedRowsArgs } from '@mui/material/TablePagination';
7
7
  import { SxProps, Theme } from '@mui/material/styles';
8
8
  export { ThemeProvider, createTheme, styled, useTheme } from '@mui/material/styles';
@@ -210,4 +210,24 @@ interface CountrySelectProps {
210
210
  }
211
211
  declare const CountrySelect: React__default.FC<CountrySelectProps>;
212
212
 
213
- export { CountrySelect, type CountryType, EmptyState, FormDialog, FormDrawer, FormPopover, FormSnackBar, IOSSwitch, ImageUploadAvatar, InputFileUpload, Loader, Logo, PhoneNumberField, Pill, SearchableSelect, SkeletonBar, StatusPill, TabPanel, TablePagination, type TablePaginationDesignProps };
213
+ type OTPFieldProps = {
214
+ length: number;
215
+ initialValue?: string;
216
+ onChange: (value: string, index?: number) => void;
217
+ onComplete?: (value: string, index?: number) => void;
218
+ /** Use password inputs (no brief reveal; React 19–safe replacement for `react-pin-input` `secret`). */
219
+ secret?: boolean;
220
+ autoSelect?: boolean;
221
+ disabled?: boolean;
222
+ /** Applied to the focused cell’s outline (`fieldset`). */
223
+ inputFocusStyle?: CSSProperties;
224
+ /** Applied to the outer `Box` (MUI `sx`). */
225
+ sx?: SxProps<Theme>;
226
+ type?: string;
227
+ inputMode?: React__default.HTMLAttributes<HTMLInputElement>["inputMode"];
228
+ /** Optional per-character filter (same idea as `react-pin-input` `regexCriteria`). */
229
+ regexCriteria?: RegExp;
230
+ };
231
+ declare const OTPField: React__default.FC<OTPFieldProps>;
232
+
233
+ export { CountrySelect, type CountryType, EmptyState, FormDialog, FormDrawer, FormPopover, FormSnackBar, IOSSwitch, ImageUploadAvatar, InputFileUpload, Loader, Logo, OTPField, type OTPFieldProps, PhoneNumberField, Pill, SearchableSelect, SkeletonBar, StatusPill, TabPanel, TablePagination, type TablePaginationDesignProps };
package/dist/index.d.ts CHANGED
@@ -2,7 +2,7 @@ import { DialogProps, DrawerProps } from '@mui/material';
2
2
  export * from '@mui/material';
3
3
  import * as react_jsx_runtime from 'react/jsx-runtime';
4
4
  import * as React from 'react';
5
- import React__default, { FC, KeyboardEvent } from 'react';
5
+ import React__default, { FC, KeyboardEvent, CSSProperties } from 'react';
6
6
  import MuiTablePagination, { LabelDisplayedRowsArgs } from '@mui/material/TablePagination';
7
7
  import { SxProps, Theme } from '@mui/material/styles';
8
8
  export { ThemeProvider, createTheme, styled, useTheme } from '@mui/material/styles';
@@ -210,4 +210,24 @@ interface CountrySelectProps {
210
210
  }
211
211
  declare const CountrySelect: React__default.FC<CountrySelectProps>;
212
212
 
213
- export { CountrySelect, type CountryType, EmptyState, FormDialog, FormDrawer, FormPopover, FormSnackBar, IOSSwitch, ImageUploadAvatar, InputFileUpload, Loader, Logo, PhoneNumberField, Pill, SearchableSelect, SkeletonBar, StatusPill, TabPanel, TablePagination, type TablePaginationDesignProps };
213
+ type OTPFieldProps = {
214
+ length: number;
215
+ initialValue?: string;
216
+ onChange: (value: string, index?: number) => void;
217
+ onComplete?: (value: string, index?: number) => void;
218
+ /** Use password inputs (no brief reveal; React 19–safe replacement for `react-pin-input` `secret`). */
219
+ secret?: boolean;
220
+ autoSelect?: boolean;
221
+ disabled?: boolean;
222
+ /** Applied to the focused cell’s outline (`fieldset`). */
223
+ inputFocusStyle?: CSSProperties;
224
+ /** Applied to the outer `Box` (MUI `sx`). */
225
+ sx?: SxProps<Theme>;
226
+ type?: string;
227
+ inputMode?: React__default.HTMLAttributes<HTMLInputElement>["inputMode"];
228
+ /** Optional per-character filter (same idea as `react-pin-input` `regexCriteria`). */
229
+ regexCriteria?: RegExp;
230
+ };
231
+ declare const OTPField: React__default.FC<OTPFieldProps>;
232
+
233
+ export { CountrySelect, type CountryType, EmptyState, FormDialog, FormDrawer, FormPopover, FormSnackBar, IOSSwitch, ImageUploadAvatar, InputFileUpload, Loader, Logo, OTPField, type OTPFieldProps, PhoneNumberField, Pill, SearchableSelect, SkeletonBar, StatusPill, TabPanel, TablePagination, type TablePaginationDesignProps };
package/dist/index.js CHANGED
@@ -42,6 +42,7 @@ __export(index_exports, {
42
42
  InputFileUpload: () => InputFileUpload_default,
43
43
  Loader: () => Loader_default,
44
44
  Logo: () => Logo_default,
45
+ OTPField: () => OTPField_default,
45
46
  PhoneNumberField: () => PhoneNumberField_default,
46
47
  Pill: () => Pill_default,
47
48
  SearchableSelect: () => SearchableSelect_default,
@@ -1653,7 +1654,6 @@ var FormDrawer = ({
1653
1654
  position: "relative",
1654
1655
  flexShrink: 0,
1655
1656
  p: 3,
1656
- borderBottom: (t) => `1px solid ${(0, import_styles14.alpha)(t.palette.divider, 0.36)}`,
1657
1657
  bgcolor: "background.paper"
1658
1658
  },
1659
1659
  children: /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(import_material14.Grid, { container: true, children: [
@@ -2274,6 +2274,178 @@ var CountrySelect = ({
2274
2274
  );
2275
2275
  };
2276
2276
  var CountrySelect_default = CountrySelect;
2277
+
2278
+ // src/OTPField.tsx
2279
+ var import_react11 = require("react");
2280
+ var import_material17 = require("@mui/material");
2281
+ var import_jsx_runtime19 = require("react/jsx-runtime");
2282
+ function toCells(s, len) {
2283
+ const slice = (s || "").slice(0, len);
2284
+ return Array.from({ length: len }, (_, i) => {
2285
+ var _a2;
2286
+ return (_a2 = slice[i]) != null ? _a2 : "";
2287
+ });
2288
+ }
2289
+ var OTPField = ({
2290
+ length,
2291
+ initialValue = "",
2292
+ onChange,
2293
+ onComplete,
2294
+ secret = false,
2295
+ autoSelect = false,
2296
+ disabled = false,
2297
+ inputFocusStyle,
2298
+ sx,
2299
+ type = "text",
2300
+ inputMode = "text",
2301
+ regexCriteria
2302
+ }) => {
2303
+ const [cells, setCells] = (0, import_react11.useState)(() => toCells(initialValue, length));
2304
+ const cellsRef = (0, import_react11.useRef)(cells);
2305
+ cellsRef.current = cells;
2306
+ const refs = (0, import_react11.useRef)([]);
2307
+ (0, import_react11.useEffect)(() => {
2308
+ setCells(toCells(initialValue, length));
2309
+ }, [initialValue, length]);
2310
+ const emit = (0, import_react11.useCallback)(
2311
+ (next, editedIndex) => {
2312
+ setCells(next);
2313
+ const joined = next.join("");
2314
+ onChange(joined, editedIndex);
2315
+ if (onComplete && joined.length === length && next.every(Boolean)) {
2316
+ onComplete(joined, length - 1);
2317
+ }
2318
+ },
2319
+ [length, onChange, onComplete]
2320
+ );
2321
+ (0, import_react11.useEffect)(() => {
2322
+ if (!autoSelect) return;
2323
+ const id = requestAnimationFrame(() => {
2324
+ var _a2;
2325
+ return (_a2 = refs.current[0]) == null ? void 0 : _a2.focus();
2326
+ });
2327
+ return () => cancelAnimationFrame(id);
2328
+ }, [autoSelect, length]);
2329
+ const charAllowed = (0, import_react11.useCallback)(
2330
+ (ch) => {
2331
+ if (!ch) return true;
2332
+ if (type === "numeric" && /\D/.test(ch)) return false;
2333
+ if (regexCriteria && !regexCriteria.test(ch)) return false;
2334
+ return true;
2335
+ },
2336
+ [regexCriteria, type]
2337
+ );
2338
+ const applyMany = (0, import_react11.useCallback)(
2339
+ (startIdx, raw) => {
2340
+ var _a2;
2341
+ const incoming = (type === "numeric" ? raw.replace(/\D/g, "") : raw).split("");
2342
+ const next = [...cellsRef.current];
2343
+ let i = startIdx;
2344
+ for (const ch of incoming) {
2345
+ if (i >= length) break;
2346
+ if (!charAllowed(ch)) return;
2347
+ next[i] = ch;
2348
+ i += 1;
2349
+ }
2350
+ const focusAt = Math.min(Math.max(i - 1, 0), length - 1);
2351
+ emit(next, focusAt);
2352
+ (_a2 = refs.current[focusAt]) == null ? void 0 : _a2.focus();
2353
+ },
2354
+ [charAllowed, emit, length, type]
2355
+ );
2356
+ const onFieldChange = (idx, e) => {
2357
+ var _a2;
2358
+ const raw = e.target.value;
2359
+ if (raw.length > 1) {
2360
+ applyMany(idx, raw);
2361
+ return;
2362
+ }
2363
+ if (raw && !charAllowed(raw)) return;
2364
+ const next = [...cellsRef.current];
2365
+ next[idx] = raw;
2366
+ emit(next, idx);
2367
+ if (raw && idx < length - 1) {
2368
+ (_a2 = refs.current[idx + 1]) == null ? void 0 : _a2.focus();
2369
+ }
2370
+ };
2371
+ const onKeyDown = (idx, e) => {
2372
+ var _a2, _b, _c;
2373
+ if (e.key === "Backspace") {
2374
+ const next = [...cellsRef.current];
2375
+ if (next[idx]) {
2376
+ next[idx] = "";
2377
+ emit(next, idx);
2378
+ } else if (idx > 0) {
2379
+ next[idx - 1] = "";
2380
+ emit(next, idx - 1);
2381
+ (_a2 = refs.current[idx - 1]) == null ? void 0 : _a2.focus();
2382
+ }
2383
+ e.preventDefault();
2384
+ } else if (e.key === "ArrowLeft" && idx > 0) {
2385
+ (_b = refs.current[idx - 1]) == null ? void 0 : _b.focus();
2386
+ e.preventDefault();
2387
+ } else if (e.key === "ArrowRight" && idx < length - 1) {
2388
+ (_c = refs.current[idx + 1]) == null ? void 0 : _c.focus();
2389
+ e.preventDefault();
2390
+ }
2391
+ };
2392
+ const onPasteFirst = (e) => {
2393
+ e.preventDefault();
2394
+ const text = e.clipboardData.getData("text").replace(/\s/g, "");
2395
+ if (!text) return;
2396
+ applyMany(0, text);
2397
+ };
2398
+ const inputType = secret ? "password" : type === "numeric" ? "tel" : "text";
2399
+ return /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
2400
+ import_material17.Box,
2401
+ {
2402
+ sx: [
2403
+ {
2404
+ display: "flex",
2405
+ gap: 1,
2406
+ flexWrap: "nowrap",
2407
+ justifyContent: "center",
2408
+ alignItems: "center"
2409
+ },
2410
+ ...sx == null ? [] : Array.isArray(sx) ? sx : [sx]
2411
+ ],
2412
+ children: Array.from({ length }).map((_, idx) => /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
2413
+ import_material17.TextField,
2414
+ {
2415
+ inputRef: (el) => {
2416
+ refs.current[idx] = el;
2417
+ },
2418
+ value: cells[idx],
2419
+ onChange: (e) => onFieldChange(idx, e),
2420
+ onKeyDown: (e) => onKeyDown(idx, e),
2421
+ onPaste: idx === 0 ? onPasteFirst : void 0,
2422
+ disabled,
2423
+ size: "small",
2424
+ type: inputType,
2425
+ sx: {
2426
+ width: 44,
2427
+ "& .MuiOutlinedInput-input": {
2428
+ textAlign: "center",
2429
+ py: 1,
2430
+ px: 0.5
2431
+ },
2432
+ ...inputFocusStyle ? {
2433
+ "& .MuiOutlinedInput-root.Mui-focused fieldset": inputFocusStyle
2434
+ } : {}
2435
+ },
2436
+ inputProps: {
2437
+ maxLength: 1,
2438
+ inputMode: inputMode != null ? inputMode : void 0,
2439
+ "aria-label": `Code character ${idx + 1} of ${length}`,
2440
+ autoComplete: "one-time-code"
2441
+ }
2442
+ },
2443
+ idx
2444
+ ))
2445
+ }
2446
+ );
2447
+ };
2448
+ var OTPField_default = OTPField;
2277
2449
  // Annotate the CommonJS export names for ESM import in node:
2278
2450
  0 && (module.exports = {
2279
2451
  CountrySelect,
@@ -2287,6 +2459,7 @@ var CountrySelect_default = CountrySelect;
2287
2459
  InputFileUpload,
2288
2460
  Loader,
2289
2461
  Logo,
2462
+ OTPField,
2290
2463
  PhoneNumberField,
2291
2464
  Pill,
2292
2465
  SearchableSelect,