@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
@@ -0,0 +1,95 @@
1
+ import { formatThousands } from './format-thousands';
2
+
3
+ export type TextFormatterOptions = {
4
+ /** Whether to allow decimal values */
5
+ allowDecimals?: boolean;
6
+ /** Whether to format with space-separated thousands */
7
+ thousandsSeparator?: boolean;
8
+ /** Maximum length for integer part (before decimal) */
9
+ maxIntegerLength?: number;
10
+ /** Maximum length for decimal part (after decimal) */
11
+ maxDecimalLength?: number;
12
+ };
13
+
14
+ /**
15
+ * Formats and validates numeric input text for amount inputs.
16
+ * Handles decimal formatting, leading zeros, and prevents multiple decimal points.
17
+ * Can optionally format with space-separated thousands.
18
+ * @returns Formatted and cleaned numeric string
19
+ *
20
+ * @example
21
+ * textFormatter('01') // '1'
22
+ * textFormatter('.5') // '0.5'
23
+ * textFormatter('1.2.3') // '1.23'
24
+ * textFormatter('1,5') // '1.5'
25
+ * textFormatter('abc123') // '123'
26
+ * textFormatter('1000000', { thousandsSeparator: true }) // '1 000 000'
27
+ * textFormatter('1234.5678', { thousandsSeparator: true }) // '1 234.5678'
28
+ * textFormatter('123456789012', { maxIntegerLength: 9 }) // '123456789'
29
+ * textFormatter('123.123456', { maxDecimalLength: 2 }) // '123.12'
30
+ */
31
+ export function textFormatter(
32
+ /** The input value to format */
33
+ value: string,
34
+ options: TextFormatterOptions = {},
35
+ ): string {
36
+ const {
37
+ allowDecimals = true,
38
+ thousandsSeparator = true,
39
+ maxIntegerLength = 9,
40
+ maxDecimalLength = 9,
41
+ } = options;
42
+ // Normalize input: convert comma to dot, remove non-digit/non-dot characters
43
+ const normalizedValue = value.replace(',', '.').replace(/[^\d.]/g, '');
44
+ let cleaned = normalizedValue;
45
+
46
+ // Remove leading zeros (except when followed by dot)
47
+ cleaned = cleaned.replace(/^0+(?=\d)/, '');
48
+
49
+ if (!allowDecimals) {
50
+ // Integer-only: remove any non-digit and apply max length
51
+ cleaned = cleaned.replace(/\D/g, '');
52
+ if (maxIntegerLength > 0 && cleaned.length > maxIntegerLength) {
53
+ cleaned = cleaned.slice(0, maxIntegerLength);
54
+ }
55
+ return thousandsSeparator ? formatThousands(cleaned) : cleaned;
56
+ }
57
+
58
+ // Handle single dot input as "0."
59
+ if (cleaned === '.') {
60
+ cleaned = '0.';
61
+ }
62
+
63
+ const firstDot = cleaned.indexOf('.');
64
+ if (firstDot !== -1) {
65
+ // Split integer and decimal parts
66
+ let integerPart = cleaned.slice(0, firstDot);
67
+ let decimalPart = cleaned.slice(firstDot + 1).replace(/\./g, '');
68
+
69
+ // Apply length limits
70
+ if (maxIntegerLength > 0 && integerPart.length > maxIntegerLength) {
71
+ integerPart = integerPart.slice(0, maxIntegerLength);
72
+ }
73
+ decimalPart = decimalPart.slice(0, maxDecimalLength);
74
+
75
+ // Ensure integer part is not empty
76
+ if (integerPart === '') integerPart = '0';
77
+
78
+ // Preserve trailing dot if user is typing
79
+ const hasTrailingDot =
80
+ normalizedValue.endsWith('.') || cleaned.endsWith('.');
81
+ cleaned =
82
+ decimalPart.length > 0
83
+ ? `${integerPart}.${decimalPart}`
84
+ : hasTrailingDot
85
+ ? `${integerPart}.`
86
+ : integerPart;
87
+ } else {
88
+ // No decimal point, just apply integer length limit
89
+ if (maxIntegerLength > 0 && cleaned.length > maxIntegerLength) {
90
+ cleaned = cleaned.slice(0, maxIntegerLength);
91
+ }
92
+ }
93
+
94
+ return thousandsSeparator ? formatThousands(cleaned) : cleaned;
95
+ }
@@ -0,0 +1 @@
1
+ export * from './is-text-children';
@@ -0,0 +1,33 @@
1
+ import React, { ReactNode } from 'react';
2
+ import { describe, it, expect } from 'vitest';
3
+ import { isTextChildren } from './is-text-children';
4
+
5
+ describe('isAllowedTextChildren', () => {
6
+ type Case = { name: string; input: unknown; expected: boolean };
7
+
8
+ const cases: Case[] = [
9
+ { name: 'string', input: 'hello', expected: true },
10
+ { name: 'empty string', input: '', expected: true },
11
+ { name: 'number', input: 42, expected: true },
12
+ { name: 'zero', input: 0, expected: true },
13
+ { name: 'NaN', input: Number.NaN, expected: true },
14
+ { name: 'array of strings', input: ['a', 'b'], expected: true },
15
+ {
16
+ name: 'React element',
17
+ input: React.createElement('View', null),
18
+ expected: false,
19
+ },
20
+ { name: 'null', input: null, expected: false },
21
+ { name: 'undefined', input: undefined, expected: false },
22
+ { name: 'array of object and string', input: [{}, 'b'], expected: false },
23
+ { name: 'boolean true', input: true, expected: false },
24
+ { name: 'boolean false', input: false, expected: false },
25
+ { name: 'object', input: { a: 1 }, expected: false },
26
+ { name: 'function', input: () => 'x', expected: false },
27
+ { name: 'symbol', input: Symbol('s'), expected: false },
28
+ ];
29
+
30
+ it.each(cases)('$name', ({ input, expected }) => {
31
+ expect(isTextChildren(input as ReactNode)).toBe(expected);
32
+ });
33
+ });
@@ -0,0 +1,9 @@
1
+ import { ReactNode } from 'react';
2
+
3
+ const isStringOrNumber = (element: ReactNode) =>
4
+ typeof element === 'string' || typeof element === 'number';
5
+
6
+ export const isTextChildren = (element: ReactNode) => {
7
+ const isArray = Array.isArray(element);
8
+ return isArray ? element.every(isStringOrNumber) : isStringOrNumber(element);
9
+ };
@@ -0,0 +1 @@
1
+ export * from './string-utils';
@@ -0,0 +1,85 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { toPascalCase } from './string-utils.js';
3
+
4
+ describe('toPascalCase', () => {
5
+ it('should convert kebab-case to PascalCase', () => {
6
+ expect(toPascalCase('arrow-up')).toBe('ArrowUp');
7
+ expect(toPascalCase('chevron-down')).toBe('ChevronDown');
8
+ expect(toPascalCase('external-link')).toBe('ExternalLink');
9
+ expect(toPascalCase('settings-alt-2')).toBe('SettingsAlt2');
10
+ });
11
+
12
+ it('should convert snake_case to PascalCase', () => {
13
+ expect(toPascalCase('arrow_up')).toBe('ArrowUp');
14
+ expect(toPascalCase('chevron_down')).toBe('ChevronDown');
15
+ expect(toPascalCase('external_link')).toBe('ExternalLink');
16
+ expect(toPascalCase('settings_alt_2')).toBe('SettingsAlt2');
17
+ });
18
+
19
+ it('should convert space-separated words to PascalCase', () => {
20
+ expect(toPascalCase('arrow up')).toBe('ArrowUp');
21
+ expect(toPascalCase('chevron down')).toBe('ChevronDown');
22
+ expect(toPascalCase('external link')).toBe('ExternalLink');
23
+ });
24
+
25
+ it('should handle mixed separators', () => {
26
+ expect(toPascalCase('arrow-up_down')).toBe('ArrowUpDown');
27
+ expect(toPascalCase('external_link-icon')).toBe('ExternalLinkIcon');
28
+ expect(toPascalCase('settings alt-2_variant')).toBe('SettingsAlt2Variant');
29
+ });
30
+
31
+ it('should handle single words', () => {
32
+ expect(toPascalCase('arrow')).toBe('Arrow');
33
+ expect(toPascalCase('icon')).toBe('Icon');
34
+ expect(toPascalCase('button')).toBe('Button');
35
+ });
36
+
37
+ it('should handle already PascalCase strings', () => {
38
+ expect(toPascalCase('ArrowUp')).toBe('Arrowup');
39
+ expect(toPascalCase('ExternalLink')).toBe('Externallink');
40
+ // Note: This behavior converts already PascalCase to lowercase except first letter
41
+ // because it treats the whole string as one word without separators
42
+ });
43
+
44
+ it('should handle empty string', () => {
45
+ expect(toPascalCase('')).toBe('');
46
+ });
47
+
48
+ it('should handle strings with numbers', () => {
49
+ expect(toPascalCase('icon-24')).toBe('Icon24');
50
+ expect(toPascalCase('chart_1')).toBe('Chart1');
51
+ expect(toPascalCase('version 2')).toBe('Version2');
52
+ expect(toPascalCase('api-v1-endpoint')).toBe('ApiV1Endpoint');
53
+ });
54
+
55
+ it('should handle multiple consecutive separators', () => {
56
+ expect(toPascalCase('arrow--up')).toBe('ArrowUp');
57
+ expect(toPascalCase('external__link')).toBe('ExternalLink');
58
+ expect(toPascalCase('icon button')).toBe('IconButton');
59
+ expect(toPascalCase('mixed-_-separators')).toBe('MixedSeparators');
60
+ });
61
+
62
+ it('should handle separators at the beginning and end', () => {
63
+ expect(toPascalCase('-arrow-up')).toBe('ArrowUp');
64
+ expect(toPascalCase('_external-link_')).toBe('ExternalLink');
65
+ expect(toPascalCase(' icon button ')).toBe('IconButton');
66
+ expect(toPascalCase('--start-end--')).toBe('StartEnd');
67
+ });
68
+
69
+ it('should handle strings with special characters mixed with separators', () => {
70
+ expect(toPascalCase('icon-24px')).toBe('Icon24px');
71
+ expect(toPascalCase('api_v2.0')).toBe('ApiV2.0');
72
+ });
73
+
74
+ it('should handle case variations in input', () => {
75
+ expect(toPascalCase('ARROW-UP')).toBe('ArrowUp');
76
+ expect(toPascalCase('ExTerNaL-LiNk')).toBe('ExternalLink');
77
+ expect(toPascalCase('mixed_CASE_string')).toBe('MixedCaseString');
78
+ });
79
+
80
+ it('should handle single character segments', () => {
81
+ expect(toPascalCase('a-b-c')).toBe('ABC');
82
+ expect(toPascalCase('x_y_z')).toBe('XYZ');
83
+ expect(toPascalCase('i o s')).toBe('IOS');
84
+ });
85
+ });
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Transforms a kebab-case or snake_case string into a PascalCase string.
3
+ * e.g., 'arrow-up' -> 'ArrowUp'
4
+ */
5
+ export function toPascalCase(str: string): string {
6
+ if (!str) return '';
7
+ return str
8
+ .split(/[-_ ]+/)
9
+ .map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
10
+ .join('');
11
+ }
@@ -0,0 +1 @@
1
+ export * from './use-merge-ref';
@@ -0,0 +1,41 @@
1
+ import { Ref, useMemo, type RefCallback } from 'react';
2
+
3
+ type PossibleRef<T> = Ref<T> | undefined;
4
+ type RefCleanup = void | (() => void);
5
+
6
+ export function assignRef<T>(ref: PossibleRef<T>, value: T | null): RefCleanup {
7
+ if (typeof ref === 'function') {
8
+ return ref(value);
9
+ } else if (typeof ref === 'object' && ref !== null && 'current' in ref) {
10
+ (ref as { current: T | null }).current = value;
11
+ }
12
+ }
13
+
14
+ export function mergeRefs<T>(...refs: PossibleRef<T>[]): RefCallback<T> {
15
+ const cleanupMap = new Map<PossibleRef<T>, () => void>();
16
+
17
+ return (node: T | null) => {
18
+ if (node === null) {
19
+ refs.forEach((ref) => {
20
+ const cleanup = cleanupMap.get(ref);
21
+ if (typeof cleanup === 'function') {
22
+ cleanup();
23
+ }
24
+ assignRef(ref, null);
25
+ });
26
+ cleanupMap.clear();
27
+ return;
28
+ }
29
+
30
+ refs.forEach((ref) => {
31
+ const cleanup = assignRef(ref, node);
32
+ if (typeof cleanup === 'function') {
33
+ cleanupMap.set(ref, cleanup);
34
+ }
35
+ });
36
+ };
37
+ }
38
+
39
+ export function useMergedRef<T>(...refs: PossibleRef<T>[]): RefCallback<T> {
40
+ return useMemo(() => mergeRefs<T>(...refs), refs);
41
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,13 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "files": [],
4
+ "include": [],
5
+ "references": [
6
+ {
7
+ "path": "./tsconfig.lib.json"
8
+ },
9
+ {
10
+ "path": "./tsconfig.spec.json"
11
+ }
12
+ ]
13
+ }
@@ -0,0 +1,29 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "baseUrl": ".",
5
+ "rootDir": "src",
6
+ "outDir": "dist",
7
+ "tsBuildInfoFile": "dist/tsconfig.lib.tsbuildinfo",
8
+ "emitDeclarationOnly": false,
9
+ "jsx": "react-jsx",
10
+ "forceConsistentCasingInFileNames": true,
11
+ "types": ["node"]
12
+ },
13
+ "include": ["src/**/*.ts", "src/**/*.tsx"],
14
+ "references": [],
15
+ "exclude": [
16
+ "vite.config.ts",
17
+ "vite.config.mts",
18
+ "vitest.config.ts",
19
+ "vitest.config.mts",
20
+ "src/**/*.test.ts",
21
+ "src/**/*.spec.ts",
22
+ "src/**/*.test.tsx",
23
+ "src/**/*.spec.tsx",
24
+ "src/**/*.test.js",
25
+ "src/**/*.spec.js",
26
+ "src/**/*.test.jsx",
27
+ "src/**/*.spec.jsx"
28
+ ]
29
+ }
@@ -0,0 +1,37 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "outDir": "./out-tsc/vitest",
5
+ "types": [
6
+ "vitest/globals",
7
+ "vitest/importMeta",
8
+ "vite/client",
9
+ "node",
10
+ "vitest"
11
+ ],
12
+ "jsx": "react-jsx",
13
+ "module": "esnext",
14
+ "moduleResolution": "bundler",
15
+ "forceConsistentCasingInFileNames": true
16
+ },
17
+ "include": [
18
+ "vite.config.ts",
19
+ "vite.config.mts",
20
+ "vitest.config.ts",
21
+ "vitest.config.mts",
22
+ "src/**/*.test.ts",
23
+ "src/**/*.spec.ts",
24
+ "src/**/*.test.tsx",
25
+ "src/**/*.spec.tsx",
26
+ "src/**/*.test.js",
27
+ "src/**/*.spec.js",
28
+ "src/**/*.test.jsx",
29
+ "src/**/*.spec.jsx",
30
+ "src/**/*.d.ts"
31
+ ],
32
+ "references": [
33
+ {
34
+ "path": "./tsconfig.lib.json"
35
+ }
36
+ ]
37
+ }
package/vite.config.ts ADDED
@@ -0,0 +1,40 @@
1
+ import { join } from 'path';
2
+ import react from '@vitejs/plugin-react';
3
+ import { defineConfig } from 'vite';
4
+ import type { LibraryFormats } from 'vite';
5
+ import dts from 'vite-plugin-dts';
6
+
7
+ export default defineConfig(() => ({
8
+ root: __dirname,
9
+ cacheDir: '../../node_modules/.vite/libs/utils-shared',
10
+ plugins: [
11
+ react(),
12
+ dts({
13
+ entryRoot: 'src',
14
+ tsconfigPath: join(__dirname, 'tsconfig.lib.json'),
15
+ }),
16
+ ],
17
+ build: {
18
+ lib: {
19
+ entry: join(__dirname, 'src/index.ts'),
20
+ name: 'utils-shared',
21
+ fileName: 'index',
22
+ formats: ['es' as LibraryFormats],
23
+ },
24
+ rollupOptions: {
25
+ external: ['clsx', 'tailwind-merge', 'react', 'react/jsx-runtime'],
26
+ },
27
+ },
28
+ test: {
29
+ watch: false,
30
+ globals: true,
31
+ silent: true,
32
+ environment: 'jsdom',
33
+ include: ['src/**/*.test.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
34
+ reporters: ['default'],
35
+ coverage: {
36
+ reportsDirectory: './test-output/vitest/coverage',
37
+ provider: 'v8' as const,
38
+ },
39
+ },
40
+ }));