@ledgerhq/lumen-utils-shared 0.0.11

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.
Files changed (63) hide show
  1. package/README.md +32 -0
  2. package/dist/index.d.ts +8 -0
  3. package/dist/index.d.ts.map +1 -0
  4. package/dist/index.js +115 -0
  5. package/dist/lib/cn.d.ts +3 -0
  6. package/dist/lib/cn.d.ts.map +1 -0
  7. package/dist/lib/create-safe-context/create-safe-context.d.ts +9 -0
  8. package/dist/lib/create-safe-context/create-safe-context.d.ts.map +1 -0
  9. package/dist/lib/create-safe-context/index.d.ts +2 -0
  10. package/dist/lib/create-safe-context/index.d.ts.map +1 -0
  11. package/dist/lib/get-object-path/get-object-path.d.ts +18 -0
  12. package/dist/lib/get-object-path/get-object-path.d.ts.map +1 -0
  13. package/dist/lib/get-object-path/index.d.ts +2 -0
  14. package/dist/lib/get-object-path/index.d.ts.map +1 -0
  15. package/dist/lib/input-formatter/font-size-formatter.d.ts +6 -0
  16. package/dist/lib/input-formatter/font-size-formatter.d.ts.map +1 -0
  17. package/dist/lib/input-formatter/format-thousands.d.ts +18 -0
  18. package/dist/lib/input-formatter/format-thousands.d.ts.map +1 -0
  19. package/dist/lib/input-formatter/index.d.ts +4 -0
  20. package/dist/lib/input-formatter/index.d.ts.map +1 -0
  21. package/dist/lib/input-formatter/text-formatter.d.ts +31 -0
  22. package/dist/lib/input-formatter/text-formatter.d.ts.map +1 -0
  23. package/dist/lib/is-text-children/index.d.ts +2 -0
  24. package/dist/lib/is-text-children/index.d.ts.map +1 -0
  25. package/dist/lib/is-text-children/is-text-children.d.ts +3 -0
  26. package/dist/lib/is-text-children/is-text-children.d.ts.map +1 -0
  27. package/dist/lib/string-utils/index.d.ts +2 -0
  28. package/dist/lib/string-utils/index.d.ts.map +1 -0
  29. package/dist/lib/string-utils/string-utils.d.ts +6 -0
  30. package/dist/lib/string-utils/string-utils.d.ts.map +1 -0
  31. package/dist/lib/use-merge-ref/index.d.ts +2 -0
  32. package/dist/lib/use-merge-ref/index.d.ts.map +1 -0
  33. package/dist/lib/use-merge-ref/use-merge-ref.d.ts +8 -0
  34. package/dist/lib/use-merge-ref/use-merge-ref.d.ts.map +1 -0
  35. package/eslint.config.mjs +22 -0
  36. package/package.json +28 -0
  37. package/project.json +7 -0
  38. package/src/index.ts +7 -0
  39. package/src/lib/cn.ts +6 -0
  40. package/src/lib/create-safe-context/create-safe-context.test.tsx +69 -0
  41. package/src/lib/create-safe-context/create-safe-context.tsx +46 -0
  42. package/src/lib/create-safe-context/index.ts +1 -0
  43. package/src/lib/get-object-path/get-object-path.test.ts +72 -0
  44. package/src/lib/get-object-path/get-object-path.ts +31 -0
  45. package/src/lib/get-object-path/index.ts +1 -0
  46. package/src/lib/input-formatter/font-size-formatter.ts +16 -0
  47. package/src/lib/input-formatter/format-thousands.test.ts +36 -0
  48. package/src/lib/input-formatter/format-thousands.ts +34 -0
  49. package/src/lib/input-formatter/index.ts +3 -0
  50. package/src/lib/input-formatter/text-formatter.test.ts +503 -0
  51. package/src/lib/input-formatter/text-formatter.ts +95 -0
  52. package/src/lib/is-text-children/index.ts +1 -0
  53. package/src/lib/is-text-children/is-text-children.test.ts +33 -0
  54. package/src/lib/is-text-children/is-text-children.ts +9 -0
  55. package/src/lib/string-utils/index.ts +1 -0
  56. package/src/lib/string-utils/string-utils.test.ts +85 -0
  57. package/src/lib/string-utils/string-utils.ts +11 -0
  58. package/src/lib/use-merge-ref/index.ts +1 -0
  59. package/src/lib/use-merge-ref/use-merge-ref.ts +41 -0
  60. package/tsconfig.json +13 -0
  61. package/tsconfig.lib.json +29 -0
  62. package/tsconfig.spec.json +37 -0
  63. package/vite.config.ts +40 -0
package/README.md ADDED
@@ -0,0 +1,32 @@
1
+ # utils-shared
2
+
3
+ Shared utility functions for the Lumen design system.
4
+
5
+ ## Contributing
6
+
7
+ ### Adding New Utilities
8
+
9
+ 1. Create your utility function in `src/lib/`
10
+ 2. Export it from `src/index.ts`
11
+ 3. Add tests in the corresponding `.spec.ts` file
12
+ 4. Update this README with usage examples
13
+
14
+ ### Development Commands
15
+
16
+ ```bash
17
+ # Build the library
18
+ npx nx build @ledgerhq/lumen-utils-shared
19
+
20
+ # Run tests
21
+ npx nx test @ledgerhq/lumen-utils-shared
22
+
23
+ # Lint code
24
+ npx nx lint @ledgerhq/lumen-utils-shared
25
+ ```
26
+
27
+ ### Guidelines
28
+
29
+ - Keep utilities pure and side-effect free
30
+ - Add comprehensive tests for all functions
31
+ - Use TypeScript for type safety
32
+ - Follow existing naming conventions
@@ -0,0 +1,8 @@
1
+ export * from './lib/string-utils/string-utils';
2
+ export * from './lib/input-formatter';
3
+ export * from './lib/create-safe-context';
4
+ export * from './lib/cn';
5
+ export * from './lib/is-text-children';
6
+ export * from './lib/use-merge-ref';
7
+ export * from './lib/get-object-path';
8
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,iCAAiC,CAAC;AAChD,cAAc,uBAAuB,CAAC;AACtC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,UAAU,CAAC;AACzB,cAAc,wBAAwB,CAAC;AACvC,cAAc,qBAAqB,CAAC;AACpC,cAAc,uBAAuB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,115 @@
1
+ import { jsx as h } from "react/jsx-runtime";
2
+ import { createContext as x, useMemo as m, useContext as y } from "react";
3
+ import { clsx as P } from "clsx";
4
+ import { twMerge as S } from "tailwind-merge";
5
+ function w(t) {
6
+ return t ? t.split(/[-_ ]+/).map((n) => n.charAt(0).toUpperCase() + n.slice(1).toLowerCase()).join("") : "";
7
+ }
8
+ const C = 48, A = 17, M = 2;
9
+ function E(t) {
10
+ const n = t.replace(/\D/g, "").length;
11
+ return Math.max(
12
+ A,
13
+ C - n * M
14
+ );
15
+ }
16
+ function f(t, n = !0) {
17
+ if (!t) return "";
18
+ const i = t.includes("."), [o, r] = t.split("."), c = o.replace(/\B(?=(\d{3})+(?!\d))/g, " ");
19
+ return i && n ? r ? `${c}.${r}` : `${c}.` : c;
20
+ }
21
+ function T(t, n = {}) {
22
+ const {
23
+ allowDecimals: i = !0,
24
+ thousandsSeparator: o = !0,
25
+ maxIntegerLength: r = 9,
26
+ maxDecimalLength: c = 9
27
+ } = n, a = t.replace(",", ".").replace(/[^\d.]/g, "");
28
+ let e = a;
29
+ if (e = e.replace(/^0+(?=\d)/, ""), !i)
30
+ return e = e.replace(/\D/g, ""), r > 0 && e.length > r && (e = e.slice(0, r)), o ? f(e) : e;
31
+ e === "." && (e = "0.");
32
+ const l = e.indexOf(".");
33
+ if (l !== -1) {
34
+ let s = e.slice(0, l), u = e.slice(l + 1).replace(/\./g, "");
35
+ r > 0 && s.length > r && (s = s.slice(0, r)), u = u.slice(0, c), s === "" && (s = "0");
36
+ const d = a.endsWith(".") || e.endsWith(".");
37
+ e = u.length > 0 ? `${s}.${u}` : d ? `${s}.` : s;
38
+ } else
39
+ r > 0 && e.length > r && (e = e.slice(0, r));
40
+ return o ? f(e) : e;
41
+ }
42
+ function _(t, n) {
43
+ const i = x(n), o = ({ children: c, value: a }) => {
44
+ const e = m(
45
+ () => a,
46
+ Object.values(a ?? {})
47
+ );
48
+ return /* @__PURE__ */ h(i.Provider, { value: e, children: c });
49
+ };
50
+ o.displayName = t + "Provider";
51
+ function r({
52
+ consumerName: c,
53
+ contextRequired: a
54
+ }) {
55
+ const e = y(i);
56
+ if (e)
57
+ return e;
58
+ if (a)
59
+ throw new Error(
60
+ `${c} must be used within ${t}`
61
+ );
62
+ return n || {};
63
+ }
64
+ return [o, r];
65
+ }
66
+ function j(...t) {
67
+ return S(P(t));
68
+ }
69
+ const p = (t) => typeof t == "string" || typeof t == "number", F = (t) => Array.isArray(t) ? t.every(p) : p(t);
70
+ function g(t, n) {
71
+ if (typeof t == "function")
72
+ return t(n);
73
+ typeof t == "object" && t !== null && "current" in t && (t.current = n);
74
+ }
75
+ function $(...t) {
76
+ const n = /* @__PURE__ */ new Map();
77
+ return (i) => {
78
+ if (i === null) {
79
+ t.forEach((o) => {
80
+ const r = n.get(o);
81
+ typeof r == "function" && r(), g(o, null);
82
+ }), n.clear();
83
+ return;
84
+ }
85
+ t.forEach((o) => {
86
+ const r = g(o, i);
87
+ typeof r == "function" && n.set(o, r);
88
+ });
89
+ };
90
+ }
91
+ function I(...t) {
92
+ return m(() => $(...t), t);
93
+ }
94
+ const z = (t, n) => {
95
+ let i = t;
96
+ for (const o of n) {
97
+ if (i == null)
98
+ return;
99
+ i = i[o];
100
+ }
101
+ return i;
102
+ };
103
+ export {
104
+ g as assignRef,
105
+ j as cn,
106
+ _ as createSafeContext,
107
+ f as formatThousands,
108
+ E as getFontSize,
109
+ z as getObjectPath,
110
+ F as isTextChildren,
111
+ $ as mergeRefs,
112
+ T as textFormatter,
113
+ w as toPascalCase,
114
+ I as useMergedRef
115
+ };
@@ -0,0 +1,3 @@
1
+ import { ClassValue } from 'clsx';
2
+ export declare function cn(...inputs: ClassValue[]): string;
3
+ //# sourceMappingURL=cn.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cn.d.ts","sourceRoot":"","sources":["../../src/lib/cn.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAQ,MAAM,MAAM,CAAC;AAGxC,wBAAgB,EAAE,CAAC,GAAG,MAAM,EAAE,UAAU,EAAE,UAEzC"}
@@ -0,0 +1,9 @@
1
+ import { FC, ReactNode } from 'react';
2
+ export declare function createSafeContext<ContextValue extends object>(rootComponentName: string, defaultContext?: ContextValue): readonly [FC<{
3
+ children: ReactNode;
4
+ value: ContextValue;
5
+ }>, <ContextRequired extends boolean = boolean>({ consumerName, contextRequired, }: {
6
+ consumerName: string;
7
+ contextRequired: ContextRequired;
8
+ }) => ContextRequired extends true ? ContextValue : Partial<ContextValue>];
9
+ //# sourceMappingURL=create-safe-context.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"create-safe-context.d.ts","sourceRoot":"","sources":["../../../src/lib/create-safe-context/create-safe-context.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAiB,EAAE,EAAE,SAAS,EAAuB,MAAM,OAAO,CAAC;AAE1E,wBAAgB,iBAAiB,CAAC,YAAY,SAAS,MAAM,EAC3D,iBAAiB,EAAE,MAAM,EACzB,cAAc,CAAC,EAAE,YAAY;cAKjB,SAAS;WACZ,YAAY;KAaG,eAAe,SAAS,OAAO,gDAGpD;IACD,YAAY,EAAE,MAAM,CAAC;IACrB,eAAe,EAAE,eAAe,CAAC;CAClC,KAAG,eAAe,SAAS,IAAI,GAAG,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC,EAgBxE"}
@@ -0,0 +1,2 @@
1
+ export * from './create-safe-context';
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/lib/create-safe-context/index.ts"],"names":[],"mappings":"AAAA,cAAc,uBAAuB,CAAC"}
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Access a nested property in an object using an array path.
3
+ *
4
+ * @param obj - The object to access
5
+ * @param path - Array of keys representing the path to the value
6
+ * @returns The value at the path, or undefined if not found
7
+ *
8
+ * @example
9
+ * ```ts
10
+ * const theme = { colors: { bg: { surface: '#fff' } } };
11
+ *
12
+ * getObjectPath(theme, ['colors', 'bg', 'surface']); // '#fff'
13
+ * getObjectPath(theme, ['colors', 'bg']); // { surface: '#fff' }
14
+ * getObjectPath(theme, ['spacings']); // undefined
15
+ * ```
16
+ */
17
+ export declare const getObjectPath: <T = unknown>(obj: Record<string, unknown>, path: string[]) => T | undefined;
18
+ //# sourceMappingURL=get-object-path.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"get-object-path.d.ts","sourceRoot":"","sources":["../../../src/lib/get-object-path/get-object-path.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,aAAa,GAAI,CAAC,GAAG,OAAO,EACvC,KAAK,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC5B,MAAM,MAAM,EAAE,KACb,CAAC,GAAG,SAWN,CAAC"}
@@ -0,0 +1,2 @@
1
+ export { getObjectPath } from './get-object-path';
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/lib/get-object-path/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC"}
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Calculates the font size based on the number of digits in the input value.
3
+ * Scales from 48px (max) to 17px (min) as digit count increases.
4
+ */
5
+ export declare function getFontSize(value: string): number;
6
+ //# sourceMappingURL=font-size-formatter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"font-size-formatter.d.ts","sourceRoot":"","sources":["../../../src/lib/input-formatter/font-size-formatter.ts"],"names":[],"mappings":"AAIA;;;GAGG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAOjD"}
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Formats a number string with space-separated thousands.
3
+ * e.g., '1000000' -> '1 000 000'
4
+ *
5
+ * @returns Formatted string with space-separated thousands
6
+ *
7
+ * @example
8
+ * formatThousands('1000') // '1 000'
9
+ * formatThousands('1000000') // '1 000 000'
10
+ * formatThousands('1234.5678') // '1 234.5678'
11
+ * formatThousands('1234.5678', false) // '1 235'
12
+ */
13
+ export declare function formatThousands(
14
+ /** The value to format */
15
+ value: string,
16
+ /** Whether to preserve decimal places */
17
+ preserveDecimals?: boolean): string;
18
+ //# sourceMappingURL=format-thousands.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"format-thousands.d.ts","sourceRoot":"","sources":["../../../src/lib/input-formatter/format-thousands.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,wBAAgB,eAAe;AAC7B,0BAA0B;AAC1B,KAAK,EAAE,MAAM;AACb,yCAAyC;AACzC,gBAAgB,UAAO,GACtB,MAAM,CAgBR"}
@@ -0,0 +1,4 @@
1
+ export { getFontSize } from './font-size-formatter';
2
+ export { formatThousands } from './format-thousands';
3
+ export { textFormatter } from './text-formatter';
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/lib/input-formatter/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AACpD,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AACrD,OAAO,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC"}
@@ -0,0 +1,31 @@
1
+ export type TextFormatterOptions = {
2
+ /** Whether to allow decimal values */
3
+ allowDecimals?: boolean;
4
+ /** Whether to format with space-separated thousands */
5
+ thousandsSeparator?: boolean;
6
+ /** Maximum length for integer part (before decimal) */
7
+ maxIntegerLength?: number;
8
+ /** Maximum length for decimal part (after decimal) */
9
+ maxDecimalLength?: number;
10
+ };
11
+ /**
12
+ * Formats and validates numeric input text for amount inputs.
13
+ * Handles decimal formatting, leading zeros, and prevents multiple decimal points.
14
+ * Can optionally format with space-separated thousands.
15
+ * @returns Formatted and cleaned numeric string
16
+ *
17
+ * @example
18
+ * textFormatter('01') // '1'
19
+ * textFormatter('.5') // '0.5'
20
+ * textFormatter('1.2.3') // '1.23'
21
+ * textFormatter('1,5') // '1.5'
22
+ * textFormatter('abc123') // '123'
23
+ * textFormatter('1000000', { thousandsSeparator: true }) // '1 000 000'
24
+ * textFormatter('1234.5678', { thousandsSeparator: true }) // '1 234.5678'
25
+ * textFormatter('123456789012', { maxIntegerLength: 9 }) // '123456789'
26
+ * textFormatter('123.123456', { maxDecimalLength: 2 }) // '123.12'
27
+ */
28
+ export declare function textFormatter(
29
+ /** The input value to format */
30
+ value: string, options?: TextFormatterOptions): string;
31
+ //# sourceMappingURL=text-formatter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"text-formatter.d.ts","sourceRoot":"","sources":["../../../src/lib/input-formatter/text-formatter.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,oBAAoB,GAAG;IACjC,sCAAsC;IACtC,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,uDAAuD;IACvD,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,uDAAuD;IACvD,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,sDAAsD;IACtD,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B,CAAC;AAEF;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,aAAa;AAC3B,gCAAgC;AAChC,KAAK,EAAE,MAAM,EACb,OAAO,GAAE,oBAAyB,GACjC,MAAM,CA4DR"}
@@ -0,0 +1,2 @@
1
+ export * from './is-text-children';
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/lib/is-text-children/index.ts"],"names":[],"mappings":"AAAA,cAAc,oBAAoB,CAAC"}
@@ -0,0 +1,3 @@
1
+ import { ReactNode } from 'react';
2
+ export declare const isTextChildren: (element: ReactNode) => boolean;
3
+ //# sourceMappingURL=is-text-children.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"is-text-children.d.ts","sourceRoot":"","sources":["../../../src/lib/is-text-children/is-text-children.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAKlC,eAAO,MAAM,cAAc,GAAI,SAAS,SAAS,YAGhD,CAAC"}
@@ -0,0 +1,2 @@
1
+ export * from './string-utils';
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/lib/string-utils/index.ts"],"names":[],"mappings":"AAAA,cAAc,gBAAgB,CAAC"}
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Transforms a kebab-case or snake_case string into a PascalCase string.
3
+ * e.g., 'arrow-up' -> 'ArrowUp'
4
+ */
5
+ export declare function toPascalCase(str: string): string;
6
+ //# sourceMappingURL=string-utils.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"string-utils.d.ts","sourceRoot":"","sources":["../../../src/lib/string-utils/string-utils.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAMhD"}
@@ -0,0 +1,2 @@
1
+ export * from './use-merge-ref';
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/lib/use-merge-ref/index.ts"],"names":[],"mappings":"AAAA,cAAc,iBAAiB,CAAC"}
@@ -0,0 +1,8 @@
1
+ import { Ref, RefCallback } from 'react';
2
+ type PossibleRef<T> = Ref<T> | undefined;
3
+ type RefCleanup = void | (() => void);
4
+ export declare function assignRef<T>(ref: PossibleRef<T>, value: T | null): RefCleanup;
5
+ export declare function mergeRefs<T>(...refs: PossibleRef<T>[]): RefCallback<T>;
6
+ export declare function useMergedRef<T>(...refs: PossibleRef<T>[]): RefCallback<T>;
7
+ export {};
8
+ //# sourceMappingURL=use-merge-ref.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-merge-ref.d.ts","sourceRoot":"","sources":["../../../src/lib/use-merge-ref/use-merge-ref.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAW,KAAK,WAAW,EAAE,MAAM,OAAO,CAAC;AAEvD,KAAK,WAAW,CAAC,CAAC,IAAI,GAAG,CAAC,CAAC,CAAC,GAAG,SAAS,CAAC;AACzC,KAAK,UAAU,GAAG,IAAI,GAAG,CAAC,MAAM,IAAI,CAAC,CAAC;AAEtC,wBAAgB,SAAS,CAAC,CAAC,EAAE,GAAG,EAAE,WAAW,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,GAAG,IAAI,GAAG,UAAU,CAM7E;AAED,wBAAgB,SAAS,CAAC,CAAC,EAAE,GAAG,IAAI,EAAE,WAAW,CAAC,CAAC,CAAC,EAAE,GAAG,WAAW,CAAC,CAAC,CAAC,CAuBtE;AAED,wBAAgB,YAAY,CAAC,CAAC,EAAE,GAAG,IAAI,EAAE,WAAW,CAAC,CAAC,CAAC,EAAE,GAAG,WAAW,CAAC,CAAC,CAAC,CAEzE"}
@@ -0,0 +1,22 @@
1
+ import baseConfig from '../../eslint.config.mjs';
2
+
3
+ export default [
4
+ ...baseConfig,
5
+ {
6
+ files: ['**/*.json'],
7
+ rules: {
8
+ '@nx/dependency-checks': [
9
+ 'error',
10
+ {
11
+ ignoredFiles: [
12
+ '{projectRoot}/eslint.config.{js,cjs,mjs}',
13
+ '{projectRoot}/vite.config.{js,ts,mjs,mts}',
14
+ ],
15
+ },
16
+ ],
17
+ },
18
+ languageOptions: {
19
+ parser: await import('jsonc-eslint-parser'),
20
+ },
21
+ },
22
+ ];
package/package.json ADDED
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "@ledgerhq/lumen-utils-shared",
3
+ "version": "0.0.11",
4
+ "type": "module",
5
+ "main": "./dist/index.js",
6
+ "module": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ },
13
+ "./package.json": "./package.json"
14
+ },
15
+ "nx": {
16
+ "tags": [
17
+ "scope:shared",
18
+ "type:utils"
19
+ ]
20
+ },
21
+ "dependencies": {
22
+ "clsx": "^2.1.1",
23
+ "tailwind-merge": "^2.6.0"
24
+ },
25
+ "peerDependencies": {
26
+ "react": "^18.0.0 || ^19.0.0"
27
+ }
28
+ }
package/project.json ADDED
@@ -0,0 +1,7 @@
1
+ {
2
+ "name": "@ledgerhq/lumen-utils-shared",
3
+ "$schema": "../../node_modules/nx/schemas/project-schema.json",
4
+ "sourceRoot": "libs/utils-shared/src",
5
+ "projectType": "library",
6
+ "tags": ["scope:shared", "type:utils"]
7
+ }
package/src/index.ts ADDED
@@ -0,0 +1,7 @@
1
+ export * from './lib/string-utils/string-utils';
2
+ export * from './lib/input-formatter';
3
+ export * from './lib/create-safe-context';
4
+ export * from './lib/cn';
5
+ export * from './lib/is-text-children';
6
+ export * from './lib/use-merge-ref';
7
+ export * from './lib/get-object-path';
package/src/lib/cn.ts ADDED
@@ -0,0 +1,6 @@
1
+ import { ClassValue, clsx } from 'clsx';
2
+ import { twMerge } from 'tailwind-merge';
3
+
4
+ export function cn(...inputs: ClassValue[]) {
5
+ return twMerge(clsx(inputs));
6
+ }
@@ -0,0 +1,69 @@
1
+ import { cleanup, render } from '@testing-library/react';
2
+ import React from 'react';
3
+ import { createSafeContext } from './create-safe-context.js';
4
+
5
+ afterEach(() => {
6
+ cleanup();
7
+ });
8
+
9
+ describe('createSafeContext', () => {
10
+ it('throws when used without provider and contextRequired = true', () => {
11
+ type Ctx = { value?: number };
12
+ const [, useSafe] = createSafeContext<Ctx>('RootComponent');
13
+
14
+ const Consumer = () => {
15
+ // This should throw because contextRequired is true and there is no Provider
16
+ useSafe({ consumerName: 'Consumer', contextRequired: true });
17
+ return null;
18
+ };
19
+
20
+ expect(() => render(React.createElement(Consumer))).toThrow(
21
+ 'Consumer must be used within RootComponent',
22
+ );
23
+ });
24
+
25
+ it('returns default context when not wrapped and contextRequired = false', () => {
26
+ type Ctx = { value: number };
27
+ const defaultCtx: Ctx = { value: 42 };
28
+ const [, useSafe] = createSafeContext<Ctx>('RootComponent', defaultCtx);
29
+
30
+ const Consumer = () => {
31
+ const ctx = useSafe({ consumerName: 'Consumer', contextRequired: false });
32
+ return React.createElement('span', { 'data-testid': 'value' }, ctx.value);
33
+ };
34
+
35
+ const { getByTestId } = render(React.createElement(Consumer));
36
+ expect(getByTestId('value').textContent).toBe('42');
37
+ });
38
+
39
+ it('provides context value via Provider when wrapped', () => {
40
+ type Ctx = { value: number };
41
+ const [Provider, useSafe] = createSafeContext<Ctx>('RootComponent');
42
+
43
+ const Consumer = () => {
44
+ const ctx = useSafe({ consumerName: 'Consumer', contextRequired: false });
45
+ return React.createElement(
46
+ 'span',
47
+ { 'data-testid': 'value' },
48
+ String(ctx.value),
49
+ );
50
+ };
51
+
52
+ const { getByTestId, rerender } = render(
53
+ React.createElement(Provider, {
54
+ value: { value: 7 },
55
+ children: React.createElement(Consumer),
56
+ }),
57
+ );
58
+ expect(getByTestId('value').textContent).toBe('7');
59
+
60
+ // Verify updates propagate
61
+ rerender(
62
+ React.createElement(Provider, {
63
+ value: { value: 8 },
64
+ children: React.createElement(Consumer),
65
+ }),
66
+ );
67
+ expect(getByTestId('value').textContent).toBe('8');
68
+ });
69
+ });
@@ -0,0 +1,46 @@
1
+ import { createContext, FC, ReactNode, useContext, useMemo } from 'react';
2
+
3
+ export function createSafeContext<ContextValue extends object>(
4
+ rootComponentName: string,
5
+ defaultContext?: ContextValue,
6
+ ) {
7
+ const Context = createContext<ContextValue | undefined>(defaultContext);
8
+
9
+ const Provider: FC<{
10
+ children: ReactNode;
11
+ value: ContextValue;
12
+ }> = ({ children, value: context }) => {
13
+ // Only re-memoize when prop values change
14
+ const memoValue = useMemo(
15
+ () => context,
16
+ Object.values(context ?? {}),
17
+ ) as ContextValue;
18
+
19
+ return <Context.Provider value={memoValue}>{children}</Context.Provider>;
20
+ };
21
+
22
+ Provider.displayName = rootComponentName + 'Provider';
23
+
24
+ function useSafeContext<ContextRequired extends boolean = boolean>({
25
+ consumerName,
26
+ contextRequired,
27
+ }: {
28
+ consumerName: string;
29
+ contextRequired: ContextRequired;
30
+ }): ContextRequired extends true ? ContextValue : Partial<ContextValue> {
31
+ const context = useContext(Context);
32
+
33
+ if (context) {
34
+ return context;
35
+ }
36
+ if (contextRequired) {
37
+ throw new Error(
38
+ `${consumerName} must be used within ${rootComponentName}`,
39
+ );
40
+ }
41
+
42
+ return (defaultContext || {}) as ContextValue;
43
+ }
44
+
45
+ return [Provider, useSafeContext] as const;
46
+ }
@@ -0,0 +1 @@
1
+ export * from './create-safe-context';
@@ -0,0 +1,72 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { getObjectPath } from './get-object-path';
3
+
4
+ describe('getObjectPath', () => {
5
+ const testObj = {
6
+ colors: {
7
+ bg: {
8
+ surface: '#ffffff',
9
+ accent: '#0000ff',
10
+ },
11
+ text: {
12
+ base: '#000000',
13
+ },
14
+ },
15
+ spacings: {
16
+ s4: 4,
17
+ s8: 8,
18
+ s16: 16,
19
+ },
20
+ nested: {
21
+ deep: {
22
+ value: 'found',
23
+ },
24
+ },
25
+ };
26
+
27
+ it('should access single level path', () => {
28
+ expect(getObjectPath(testObj, ['spacings'])).toEqual({
29
+ s4: 4,
30
+ s8: 8,
31
+ s16: 16,
32
+ });
33
+ });
34
+
35
+ it('should access two level path', () => {
36
+ expect(getObjectPath(testObj, ['colors', 'bg'])).toEqual({
37
+ surface: '#ffffff',
38
+ accent: '#0000ff',
39
+ });
40
+ });
41
+
42
+ it('should access three level path', () => {
43
+ expect(getObjectPath(testObj, ['colors', 'bg', 'surface'])).toBe('#ffffff');
44
+ });
45
+
46
+ it('should access deeply nested path', () => {
47
+ expect(getObjectPath(testObj, ['nested', 'deep', 'value'])).toBe('found');
48
+ });
49
+
50
+ it('should return undefined for non-existent path', () => {
51
+ expect(getObjectPath(testObj, ['nonexistent'])).toBeUndefined();
52
+ expect(getObjectPath(testObj, ['colors', 'nonexistent'])).toBeUndefined();
53
+ expect(
54
+ getObjectPath(testObj, ['colors', 'bg', 'nonexistent']),
55
+ ).toBeUndefined();
56
+ });
57
+
58
+ it('should return undefined for path through non-object', () => {
59
+ expect(
60
+ getObjectPath(testObj, ['spacings', 's4', 'invalid']),
61
+ ).toBeUndefined();
62
+ });
63
+
64
+ it('should handle empty path', () => {
65
+ expect(getObjectPath(testObj, [])).toEqual(testObj);
66
+ });
67
+
68
+ it('should work with type parameter', () => {
69
+ const result = getObjectPath<string>(testObj, ['colors', 'bg', 'surface']);
70
+ expect(result).toBe('#ffffff');
71
+ });
72
+ });