@ledgerhq/lumen-utils-shared 0.1.6 → 0.1.8

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/dist/index.d.ts CHANGED
@@ -6,6 +6,7 @@ export * from './lib/cn';
6
6
  export * from './lib/isTextChildren';
7
7
  export * from './lib/useMergeRef';
8
8
  export * from './lib/getObjectPath';
9
+ export * from './lib/resolveProps';
9
10
  export * from './lib/components';
10
11
  export * from './lib/throttle';
11
12
  export * from './lib/debounce';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,eAAe,CAAC;AAC9B,cAAc,qBAAqB,CAAC;AACpC,cAAc,sBAAsB,CAAC;AACrC,cAAc,eAAe,CAAC;AAC9B,cAAc,UAAU,CAAC;AACzB,cAAc,sBAAsB,CAAC;AACrC,cAAc,mBAAmB,CAAC;AAClC,cAAc,qBAAqB,CAAC;AACpC,cAAc,kBAAkB,CAAC;AACjC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,YAAY,CAAC;AAC3B,cAAc,oBAAoB,CAAC;AACnC,cAAc,mCAAmC,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,eAAe,CAAC;AAC9B,cAAc,qBAAqB,CAAC;AACpC,cAAc,sBAAsB,CAAC;AACrC,cAAc,eAAe,CAAC;AAC9B,cAAc,UAAU,CAAC;AACzB,cAAc,sBAAsB,CAAC;AACrC,cAAc,mBAAmB,CAAC;AAClC,cAAc,qBAAqB,CAAC;AACpC,cAAc,oBAAoB,CAAC;AACnC,cAAc,kBAAkB,CAAC;AACjC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,YAAY,CAAC;AAC3B,cAAc,oBAAoB,CAAC;AACnC,cAAc,mCAAmC,CAAC"}
package/dist/index.js CHANGED
@@ -1,143 +1,144 @@
1
- import { jsx as k } from "react/jsx-runtime";
2
- import { createContext as W, useRef as F, useContext as w, useMemo as I } from "react";
3
- import { clsx as B } from "clsx";
4
- import { twMerge as R } from "tailwind-merge";
5
- function G(t) {
1
+ import { jsx as v } from "react/jsx-runtime";
2
+ import { createContext as F, useRef as R, useContext as W, useMemo as C } from "react";
3
+ import { clsx as w } from "clsx";
4
+ import { twMerge as B } from "tailwind-merge";
5
+ function et(t) {
6
6
  return t ? t.split(/[-_ ]+/).map((e) => e.charAt(0).toUpperCase() + e.slice(1).toLowerCase()).join("") : "";
7
7
  }
8
8
  const _ = {
9
9
  md: { max: 48, min: 17, scale: 2, startAt: 0 },
10
10
  sm: { max: 28, min: 12, scale: 2, startAt: 6 }
11
11
  };
12
- function J(t, e = "md") {
13
- const { max: r, min: o, scale: s, startAt: l } = _[e], i = t.replace(/\D/g, "").length;
14
- return Math.max(o, r - Math.max(0, i - l) * s);
12
+ function nt(t, e = "md") {
13
+ const { max: r, min: n, scale: i, startAt: s } = _[e], o = t.replace(/\D/g, "").length;
14
+ return Math.max(n, r - Math.max(0, o - s) * i);
15
15
  }
16
- function A(t, e = !0) {
16
+ function z(t, e = !0) {
17
17
  if (!t) return "";
18
- const r = t.includes("."), [o, s] = t.split("."), l = o.replace(/\B(?=(\d{3})+(?!\d))/g, " ");
19
- return r && e ? s ? `${l}.${s}` : `${l}.` : l;
18
+ const r = t.includes("."), [n, i] = t.split("."), s = n.replace(/\B(?=(\d{3})+(?!\d))/g, " ");
19
+ return r && e ? i ? `${s}.${i}` : `${s}.` : s;
20
20
  }
21
- function Q(t, e = {}) {
21
+ const Z = (t) => t.replaceAll(",", ".").replaceAll(/[^\d.]/g, ""), q = (t) => t.replace(/^0+(?=\d)/, ""), T = (t, e) => e > 0 ? t.slice(0, e) : t, K = (t, e) => T(t.replaceAll(/\D/g, ""), e), N = (t, e, r) => {
22
+ if (t === ".") return "0.";
23
+ const n = t.indexOf(".");
24
+ if (n === -1) return T(t, e);
25
+ const i = T(t.slice(0, n), e) || "0", s = t.slice(n + 1).replaceAll(".", "").slice(0, r);
26
+ return s.length > 0 ? `${i}.${s}` : t.endsWith(".") ? `${i}.` : i;
27
+ };
28
+ function rt(t, e = {}) {
22
29
  const {
23
30
  allowDecimals: r = !0,
24
- thousandsSeparator: o = !0,
25
- maxIntegerLength: s = 9,
26
- maxDecimalLength: l = 9
27
- } = e, i = t.replace(",", ".").replace(/[^\d.]/g, "");
28
- let n = i;
29
- if (n = n.replace(/^0+(?=\d)/, ""), !r)
30
- return n = n.replace(/\D/g, ""), s > 0 && n.length > s && (n = n.slice(0, s)), o ? A(n) : n;
31
- n === "." && (n = "0.");
32
- const u = n.indexOf(".");
33
- if (u === -1)
34
- s > 0 && n.length > s && (n = n.slice(0, s));
35
- else {
36
- let a = n.slice(0, u), f = n.slice(u + 1).replace(/\./g, "");
37
- s > 0 && a.length > s && (a = a.slice(0, s)), f = f.slice(0, l), a === "" && (a = "0");
38
- const h = i.endsWith(".") || n.endsWith(".");
39
- n = f.length > 0 ? `${a}.${f}` : h ? `${a}.` : a;
40
- }
41
- return o ? A(n) : n;
31
+ thousandsSeparator: n = !0,
32
+ maxIntegerLength: i = 9,
33
+ maxDecimalLength: s = 9,
34
+ decimalSeparator: o = "."
35
+ } = e, c = q(Z(t)), a = r ? N(c, i, s) : K(c, i), d = n ? z(a) : a;
36
+ return o === "." ? d : d.replaceAll(".", o);
42
37
  }
43
- function q(t, e) {
38
+ function H(t, e) {
44
39
  if (Object.is(t, e)) return !0;
45
- const r = t, o = e, s = Object.keys(r), l = Object.keys(o);
46
- return s.length !== l.length ? !1 : s.every(
47
- (i) => Object.prototype.hasOwnProperty.call(o, i) && Object.is(r[i], o[i])
40
+ const r = t, n = e, i = Object.keys(r), s = Object.keys(n);
41
+ return i.length !== s.length ? !1 : i.every(
42
+ (o) => Object.prototype.hasOwnProperty.call(n, o) && Object.is(r[o], n[o])
48
43
  );
49
44
  }
50
- function z(t, e) {
51
- const r = W(e), o = ({ children: l, value: i }) => {
52
- const n = F(i);
53
- return q(n.current, i) || (n.current = i), /* @__PURE__ */ k(r.Provider, { value: n.current, children: l });
45
+ function U(t, e) {
46
+ const r = F(e), n = ({ children: s, value: o }) => {
47
+ const c = R(o);
48
+ return H(c.current, o) || (c.current = o), /* @__PURE__ */ v(r.Provider, { value: c.current, children: s });
54
49
  };
55
- o.displayName = t + "Provider";
56
- function s({
57
- consumerName: l,
58
- contextRequired: i
50
+ n.displayName = t + "Provider";
51
+ function i({
52
+ consumerName: s,
53
+ contextRequired: o
59
54
  }) {
60
- const n = w(r);
61
- if (n)
62
- return n;
63
- if (i)
55
+ const c = W(r);
56
+ if (c)
57
+ return c;
58
+ if (o)
64
59
  throw new Error(
65
- `${l} must be used within ${t}`
60
+ `${s} must be used within ${t}`
66
61
  );
67
62
  return e || {};
68
63
  }
69
- return [o, s];
64
+ return [n, i];
70
65
  }
71
- const [X, K] = z("Disabled", { disabled: !1 }), tt = ({
66
+ const [ot, Y] = U("Disabled", { disabled: !1 }), it = ({
72
67
  consumerName: t,
73
68
  contextRequired: e,
74
69
  mergeWith: r
75
70
  }) => {
76
- const o = K({
71
+ const n = Y({
77
72
  consumerName: t,
78
73
  contextRequired: e ?? !1
79
74
  });
80
- return !!(r?.disabled || o.disabled);
75
+ return !!(r?.disabled || n.disabled);
81
76
  };
82
- function et(...t) {
83
- return R(B(t));
77
+ function st(...t) {
78
+ return B(w(t));
84
79
  }
85
- const O = (t) => typeof t == "string" || typeof t == "number", nt = (t) => Array.isArray(t) ? t.every(O) : O(t);
80
+ const O = (t) => typeof t == "string" || typeof t == "number", ct = (t) => Array.isArray(t) ? t.every(O) : O(t);
86
81
  function S(t, e) {
87
82
  if (typeof t == "function")
88
83
  return t(e);
89
84
  typeof t == "object" && t !== null && "current" in t && (t.current = e);
90
85
  }
91
- function N(...t) {
86
+ function G(...t) {
92
87
  const e = /* @__PURE__ */ new Map();
93
88
  return (r) => {
94
89
  if (r === null) {
95
- t.forEach((o) => {
96
- const s = e.get(o);
97
- typeof s == "function" && s(), S(o, null);
90
+ t.forEach((n) => {
91
+ const i = e.get(n);
92
+ typeof i == "function" && i(), S(n, null);
98
93
  }), e.clear();
99
94
  return;
100
95
  }
101
- t.forEach((o) => {
102
- const s = S(o, r);
103
- typeof s == "function" && e.set(o, s);
96
+ t.forEach((n) => {
97
+ const i = S(n, r);
98
+ typeof i == "function" && e.set(n, i);
104
99
  });
105
100
  };
106
101
  }
107
- function rt(...t) {
108
- return I(() => N(...t), t);
102
+ function lt(...t) {
103
+ return C(() => G(...t), t);
109
104
  }
110
- const ot = (t, e) => {
105
+ const at = (t, e) => {
111
106
  let r = t;
112
- for (const o of e) {
107
+ for (const n of e) {
113
108
  if (r == null)
114
109
  return;
115
- r = r[o];
110
+ r = r[n];
116
111
  }
117
112
  return r;
118
- }, it = ({
113
+ };
114
+ function ut(t) {
115
+ return function(r, n) {
116
+ return t[r][n];
117
+ };
118
+ }
119
+ const ft = ({
119
120
  currentStep: t,
120
121
  totalSteps: e,
121
122
  size: r,
122
- label: o,
123
- strokeWidth: s = 4,
124
- arcPercentage: l = 0.75
123
+ label: n,
124
+ strokeWidth: i = 4,
125
+ arcPercentage: s = 0.75
125
126
  }) => {
126
- const i = Math.min(Math.max(t, 0), e), n = o ?? `${i}/${e}`, u = e <= 0 ? 0 : i / e, a = (r - s) / 2, f = r / 2, h = r / 2, d = 2 * Math.PI * a, g = d * l, b = g * (1 - u), p = i <= 0, D = `${g} ${d}`, P = p ? g - 2 : b;
127
+ const o = Math.min(Math.max(t, 0), e), c = n ?? `${o}/${e}`, a = e <= 0 ? 0 : o / e, d = (r - i) / 2, g = r / 2, p = r / 2, u = 2 * Math.PI * d, m = u * s, P = m * (1 - a), h = o <= 0, D = `${m} ${u}`, b = h ? m - 2 : P;
127
128
  return {
128
- displayLabel: n,
129
- progress: u,
130
- r: a,
131
- cx: f,
132
- cy: h,
133
- circumference: d,
134
- trackArcLength: g,
135
- trackDashArray: `${g} ${d}`,
129
+ displayLabel: c,
130
+ progress: a,
131
+ r: d,
132
+ cx: g,
133
+ cy: p,
134
+ circumference: u,
135
+ trackArcLength: m,
136
+ trackDashArray: `${m} ${u}`,
136
137
  progressDashArray: D,
137
- progressDashOffset: P,
138
- showMinimalDot: p
138
+ progressDashOffset: b,
139
+ showMinimalDot: h
139
140
  };
140
- }, st = (t) => I(
141
+ }, dt = (t) => C(
141
142
  () => ({
142
143
  integerPart: Array.from(t.integerPart, (e) => ({
143
144
  value: e,
@@ -149,57 +150,57 @@ const ot = (t, e) => {
149
150
  })) : []
150
151
  }),
151
152
  [t.integerPart, t.decimalPart]
152
- ), ct = (t, e, r) => {
153
+ ), mt = (t, e, r) => {
153
154
  if (e) return r;
154
- const o = t.decimalPart ? `${t.decimalSeparator}${t.decimalPart}` : "", s = `${t.integerPart}${o}`;
155
- return t.currencyPosition === "end" ? `${s} ${t.currencyText}` : `${t.currencyText} ${s}`;
155
+ const n = t.decimalPart ? `${t.decimalSeparator}${t.decimalPart}` : "", i = `${t.integerPart}${n}`;
156
+ return t.currencyPosition === "end" ? `${i} ${t.currencyText}` : `${t.currencyText} ${i}`;
156
157
  };
157
- function Z(t, e, r = {}) {
158
- const { leading: o = !1, trailing: s = !0, maxWait: l } = r;
159
- let i = null, n = null, u = null, a = 0, f = null, h = null, d;
160
- const g = l !== void 0, b = g ? Math.max(l, e) : 0, p = (c) => {
161
- a = c;
162
- const m = f, y = h;
163
- return f = null, h = null, d = t.apply(y, m), d;
164
- }, D = (c) => {
165
- const m = u ? c - u : 0;
166
- return e - m;
167
- }, P = (c) => {
168
- const m = u ? c - u : e, y = c - a;
169
- return u === null || m >= e || m < 0 || g && y >= b;
170
- }, $ = (c) => (i = null, s && f ? p(c) : (f = null, h = null, d)), x = () => {
171
- const c = Date.now();
172
- if (P(c)) {
173
- $(c);
158
+ function J(t, e, r = {}) {
159
+ const { leading: n = !1, trailing: i = !0, maxWait: s } = r;
160
+ let o = null, c = null, a = null, d = 0, g = null, p = null, u;
161
+ const m = s !== void 0, P = m ? Math.max(s, e) : 0, h = (l) => {
162
+ d = l;
163
+ const f = g, x = p;
164
+ return g = null, p = null, u = t.apply(x, f), u;
165
+ }, D = (l) => {
166
+ const f = a ? l - a : 0;
167
+ return e - f;
168
+ }, b = (l) => {
169
+ const f = a ? l - a : e, x = l - d;
170
+ return a === null || f >= e || f < 0 || m && x >= P;
171
+ }, $ = (l) => (o = null, i && g ? h(l) : (g = null, p = null, u)), y = () => {
172
+ const l = Date.now();
173
+ if (b(l)) {
174
+ $(l);
174
175
  return;
175
176
  }
176
- i = setTimeout(x, D(c));
177
+ o = setTimeout(y, D(l));
177
178
  }, M = () => {
178
- const c = Date.now();
179
- n = null, f && (p(c), i !== null && (clearTimeout(i), i = setTimeout(x, e)));
180
- }, v = (c) => (a = c, i = setTimeout(x, e), g && (n = setTimeout(M, b)), o ? p(c) : d), E = () => {
181
- i !== null && clearTimeout(i), n !== null && clearTimeout(n), a = 0, f = null, h = null, u = null, i = null, n = null;
182
- }, L = () => i === null && n === null ? d : $(Date.now()), j = () => i !== null || n !== null, T = function(...c) {
183
- const m = Date.now(), y = P(m);
184
- if (f = c, h = this, u = m, y) {
185
- if (i === null)
186
- return v(m);
187
- if (g)
188
- return clearTimeout(i), i = setTimeout(x, e), p(m);
179
+ const l = Date.now();
180
+ c = null, g && (h(l), o !== null && (clearTimeout(o), o = setTimeout(y, e)));
181
+ }, E = (l) => (d = l, o = setTimeout(y, e), m && (c = setTimeout(M, P)), n ? h(l) : u), L = () => {
182
+ o !== null && clearTimeout(o), c !== null && clearTimeout(c), d = 0, g = null, p = null, a = null, o = null, c = null;
183
+ }, j = () => o === null && c === null ? u : $(Date.now()), k = () => o !== null || c !== null, A = function(...l) {
184
+ const f = Date.now(), x = b(f);
185
+ if (g = l, p = this, a = f, x) {
186
+ if (o === null)
187
+ return E(f);
188
+ if (m)
189
+ return clearTimeout(o), o = setTimeout(y, e), h(f);
189
190
  }
190
- return i === null && (i = setTimeout(x, e)), d;
191
+ return o === null && (o = setTimeout(y, e)), u;
191
192
  };
192
- return T.cancel = E, T.flush = L, T.pending = j, T;
193
+ return A.cancel = L, A.flush = j, A.pending = k, A;
193
194
  }
194
- function lt(t, e, r = {}) {
195
- const { leading: o = !0, trailing: s = !0 } = r;
196
- return Z(t, e, {
197
- leading: o,
198
- trailing: s,
195
+ function gt(t, e, r = {}) {
196
+ const { leading: n = !0, trailing: i = !0 } = r;
197
+ return J(t, e, {
198
+ leading: n,
199
+ trailing: i,
199
200
  maxWait: e
200
201
  });
201
202
  }
202
- const at = ({
203
+ const pt = ({
203
204
  onClick: t,
204
205
  disabled: e
205
206
  }) => t ? e ? {
@@ -209,43 +210,44 @@ const at = ({
209
210
  } : {
210
211
  role: "button",
211
212
  tabIndex: 0,
212
- onKeyDown: (o) => {
213
- (o.key === "Enter" || o.key === " ") && (o.preventDefault(), t?.(o));
213
+ onKeyDown: (n) => {
214
+ (n.key === "Enter" || n.key === " ") && (n.preventDefault(), t?.(n));
214
215
  },
215
216
  onClick: t
216
- } : void 0, C = " ", ut = ({
217
+ } : void 0, I = " ", ht = ({
217
218
  label: t,
218
219
  placeholder: e
219
220
  }) => {
220
221
  const r = typeof e == "string" && e.trim().length > 0 ? e : void 0;
221
222
  return t ? {
222
- inputPlaceholder: r ?? C,
223
+ inputPlaceholder: r ?? I,
223
224
  labelStaysFloatedWithPlaceholder: r !== void 0
224
225
  } : {
225
- inputPlaceholder: e ?? C,
226
+ inputPlaceholder: e ?? I,
226
227
  labelStaysFloatedWithPlaceholder: !1
227
228
  };
228
229
  };
229
230
  export {
230
- X as DisabledProvider,
231
+ ot as DisabledProvider,
231
232
  S as assignRef,
232
- ct as buildAriaLabel,
233
- et as cn,
234
- z as createSafeContext,
235
- Z as debounce,
236
- A as formatThousands,
237
- at as getButtonA11yProps,
238
- J as getFontSize,
239
- ot as getObjectPath,
240
- it as getStepperCalculations,
241
- nt as isTextChildren,
242
- N as mergeRefs,
243
- ut as resolveBaseInputPlaceholder,
244
- q as shallowEqual,
245
- Q as textFormatter,
246
- lt as throttle,
247
- G as toPascalCase,
248
- tt as useDisabledContext,
249
- rt as useMergedRef,
250
- st as useSplitText
233
+ mt as buildAriaLabel,
234
+ st as cn,
235
+ ut as createPropsResolver,
236
+ U as createSafeContext,
237
+ J as debounce,
238
+ z as formatThousands,
239
+ pt as getButtonA11yProps,
240
+ nt as getFontSize,
241
+ at as getObjectPath,
242
+ ft as getStepperCalculations,
243
+ ct as isTextChildren,
244
+ G as mergeRefs,
245
+ ht as resolveBaseInputPlaceholder,
246
+ H as shallowEqual,
247
+ rt as textFormatter,
248
+ gt as throttle,
249
+ et as toPascalCase,
250
+ it as useDisabledContext,
251
+ lt as useMergedRef,
252
+ dt as useSplitText
251
253
  };
@@ -7,11 +7,20 @@ export type TextFormatterOptions = {
7
7
  maxIntegerLength?: number;
8
8
  /** Maximum length for decimal part (after decimal) */
9
9
  maxDecimalLength?: number;
10
+ /**
11
+ * Character used to display the decimal separator.
12
+ * Input parsing always accepts both `.` and `,` regardless of this value.
13
+ */
14
+ decimalSeparator?: '.' | ',';
10
15
  };
11
16
  /**
12
17
  * 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.
18
+ *
19
+ * Parsing is always lenient: both `.` and `,` are accepted as the decimal
20
+ * separator on input, so the result stays correct regardless of the locale
21
+ * keyboard. The `decimalSeparator` option only controls how the result is
22
+ * displayed.
23
+ *
15
24
  * @returns Formatted and cleaned numeric string
16
25
  *
17
26
  * @example
@@ -19,9 +28,11 @@ export type TextFormatterOptions = {
19
28
  * textFormatter('.5') // '0.5'
20
29
  * textFormatter('1.2.3') // '1.23'
21
30
  * textFormatter('1,5') // '1.5'
31
+ * textFormatter('1,5', { decimalSeparator: ',' }) // '1,5'
22
32
  * textFormatter('abc123') // '123'
23
33
  * textFormatter('1000000', { thousandsSeparator: true }) // '1 000 000'
24
34
  * textFormatter('1234.5678', { thousandsSeparator: true }) // '1 234.5678'
35
+ * textFormatter('1234.5', { decimalSeparator: ',' }) // '1 234,5'
25
36
  * textFormatter('123456789012', { maxIntegerLength: 9 }) // '123456789'
26
37
  * textFormatter('123.123456', { maxDecimalLength: 2 }) // '123.12'
27
38
  */
@@ -1 +1 @@
1
- {"version":3,"file":"textFormatter.d.ts","sourceRoot":"","sources":["../../../src/lib/inputFormatter/textFormatter.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"}
1
+ {"version":3,"file":"textFormatter.d.ts","sourceRoot":"","sources":["../../../src/lib/inputFormatter/textFormatter.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;IAC1B;;;OAGG;IACH,gBAAgB,CAAC,EAAE,GAAG,GAAG,GAAG,CAAC;CAC9B,CAAC;AAyCF;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,aAAa;AAC3B,gCAAgC;AAChC,KAAK,EAAE,MAAM,EACb,OAAO,GAAE,oBAAyB,GACjC,MAAM,CAsBR"}
@@ -0,0 +1,2 @@
1
+ export * from './resolveProps';
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/lib/resolveProps/index.ts"],"names":[],"mappings":"AAAA,cAAc,gBAAgB,CAAC"}
@@ -0,0 +1,4 @@
1
+ export type PropsMap<Variant extends string, Props> = Record<Variant, Props>;
2
+ export type ContextPropsMap = Record<string, PropsMap<string, unknown>>;
3
+ export declare function createPropsResolver<T extends ContextPropsMap>(config: T): <Context extends keyof T, Variant extends keyof T[Context]>(context: Context, variant: Variant) => T[Context][Variant];
4
+ //# sourceMappingURL=resolveProps.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resolveProps.d.ts","sourceRoot":"","sources":["../../../src/lib/resolveProps/resolveProps.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,QAAQ,CAAC,OAAO,SAAS,MAAM,EAAE,KAAK,IAAI,MAAM,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;AAC7E,MAAM,MAAM,eAAe,GAAG,MAAM,CAAC,MAAM,EAAE,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;AAExE,wBAAgB,mBAAmB,CAAC,CAAC,SAAS,eAAe,EAAE,MAAM,EAAE,CAAC,IAEpE,OAAO,SAAS,MAAM,CAAC,EACvB,OAAO,SAAS,MAAM,CAAC,CAAC,OAAO,CAAC,EAChC,SAAS,OAAO,EAAE,SAAS,OAAO,yBAGrC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ledgerhq/lumen-utils-shared",
3
- "version": "0.1.6",
3
+ "version": "0.1.8",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.js",
package/src/index.ts CHANGED
@@ -6,6 +6,7 @@ export * from './lib/cn';
6
6
  export * from './lib/isTextChildren';
7
7
  export * from './lib/useMergeRef';
8
8
  export * from './lib/getObjectPath';
9
+ export * from './lib/resolveProps';
9
10
  export * from './lib/components';
10
11
  export * from './lib/throttle';
11
12
  export * from './lib/debounce';
@@ -1,5 +1,5 @@
1
1
  import { describe, it, expect } from 'vitest';
2
- import { textFormatter } from './textFormatter.js';
2
+ import { textFormatter, type TextFormatterOptions } from './textFormatter.js';
3
3
 
4
4
  describe('textFormatter', () => {
5
5
  describe('decimal handling with allowDecimals=true (default)', () => {
@@ -93,6 +93,64 @@ describe('textFormatter', () => {
93
93
  });
94
94
  });
95
95
 
96
+ describe('decimalSeparator display', () => {
97
+ type Case = {
98
+ name: string;
99
+ input: string;
100
+ expected: string;
101
+ options?: TextFormatterOptions;
102
+ };
103
+
104
+ const commaSeparatorCases: Case[] = [
105
+ {
106
+ name: 'display dot decimal as comma',
107
+ input: '1.5',
108
+ expected: '1,5',
109
+ options: { decimalSeparator: ',' },
110
+ },
111
+ {
112
+ name: 'display comma input as comma',
113
+ input: '1,5',
114
+ expected: '1,5',
115
+ options: { decimalSeparator: ',' },
116
+ },
117
+ {
118
+ name: 'comma decimal with space thousands',
119
+ input: '1234.5',
120
+ expected: '1 234,5',
121
+ options: { decimalSeparator: ',' },
122
+ },
123
+ {
124
+ name: 'preserve trailing separator while typing',
125
+ input: '12.',
126
+ expected: '12,',
127
+ options: { decimalSeparator: ',' },
128
+ },
129
+ {
130
+ name: 'no decimal part is unaffected',
131
+ input: '1234',
132
+ expected: '1 234',
133
+ options: { decimalSeparator: ',' },
134
+ },
135
+ {
136
+ name: 'integer-only mode is unaffected by comma separator',
137
+ input: '1.5',
138
+ expected: '15',
139
+ options: { decimalSeparator: ',', allowDecimals: false },
140
+ },
141
+ {
142
+ name: 'explicit dot separator matches default',
143
+ input: '1234.5',
144
+ expected: '1 234.5',
145
+ options: { decimalSeparator: '.' },
146
+ },
147
+ ];
148
+
149
+ it.each(commaSeparatorCases)('$name', ({ input, expected, options }) => {
150
+ expect(textFormatter(input, options)).toBe(expected);
151
+ });
152
+ });
153
+
96
154
  describe('non-numeric character removal', () => {
97
155
  type Case = { name: string; input: string; expected: string };
98
156
 
@@ -9,12 +9,60 @@ export type TextFormatterOptions = {
9
9
  maxIntegerLength?: number;
10
10
  /** Maximum length for decimal part (after decimal) */
11
11
  maxDecimalLength?: number;
12
+ /**
13
+ * Character used to display the decimal separator.
14
+ * Input parsing always accepts both `.` and `,` regardless of this value.
15
+ */
16
+ decimalSeparator?: '.' | ',';
17
+ };
18
+
19
+ /** Keep only digits and dots; commas are treated as decimal separators. */
20
+ const sanitize = (value: string): string =>
21
+ value.replaceAll(',', '.').replaceAll(/[^\d.]/g, '');
22
+
23
+ /** Drop leading zeros, keeping one when it precedes a dot or stands alone (e.g. "0", "0.5"). */
24
+ const stripLeadingZeros = (value: string): string =>
25
+ value.replace(/^0+(?=\d)/, '');
26
+
27
+ /** Truncate to `max` characters. A `max` of 0 disables the limit. */
28
+ const limitIntegerLength = (value: string, max: number): string =>
29
+ max > 0 ? value.slice(0, max) : value;
30
+
31
+ const formatIntegerOnly = (value: string, maxIntegerLength: number): string =>
32
+ limitIntegerLength(value.replaceAll(/\D/g, ''), maxIntegerLength);
33
+
34
+ const formatDecimal = (
35
+ value: string,
36
+ maxIntegerLength: number,
37
+ maxDecimalLength: number,
38
+ ): string => {
39
+ // A lone separator becomes "0." so the user can keep typing decimals.
40
+ if (value === '.') return '0.';
41
+
42
+ const firstDot = value.indexOf('.');
43
+ if (firstDot === -1) return limitIntegerLength(value, maxIntegerLength);
44
+
45
+ const integerPart =
46
+ limitIntegerLength(value.slice(0, firstDot), maxIntegerLength) || '0';
47
+ // Extra dots are ignored, e.g. the decimal part of "1.2.3" is "23".
48
+ const decimalPart = value
49
+ .slice(firstDot + 1)
50
+ .replaceAll('.', '')
51
+ .slice(0, maxDecimalLength);
52
+
53
+ if (decimalPart.length > 0) return `${integerPart}.${decimalPart}`;
54
+ // Preserve a trailing dot mid-typing (e.g. "12.").
55
+ return value.endsWith('.') ? `${integerPart}.` : integerPart;
12
56
  };
13
57
 
14
58
  /**
15
59
  * 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.
60
+ *
61
+ * Parsing is always lenient: both `.` and `,` are accepted as the decimal
62
+ * separator on input, so the result stays correct regardless of the locale
63
+ * keyboard. The `decimalSeparator` option only controls how the result is
64
+ * displayed.
65
+ *
18
66
  * @returns Formatted and cleaned numeric string
19
67
  *
20
68
  * @example
@@ -22,9 +70,11 @@ export type TextFormatterOptions = {
22
70
  * textFormatter('.5') // '0.5'
23
71
  * textFormatter('1.2.3') // '1.23'
24
72
  * textFormatter('1,5') // '1.5'
73
+ * textFormatter('1,5', { decimalSeparator: ',' }) // '1,5'
25
74
  * textFormatter('abc123') // '123'
26
75
  * textFormatter('1000000', { thousandsSeparator: true }) // '1 000 000'
27
76
  * textFormatter('1234.5678', { thousandsSeparator: true }) // '1 234.5678'
77
+ * textFormatter('1234.5', { decimalSeparator: ',' }) // '1 234,5'
28
78
  * textFormatter('123456789012', { maxIntegerLength: 9 }) // '123456789'
29
79
  * textFormatter('123.123456', { maxDecimalLength: 2 }) // '123.12'
30
80
  */
@@ -38,58 +88,20 @@ export function textFormatter(
38
88
  thousandsSeparator = true,
39
89
  maxIntegerLength = 9,
40
90
  maxDecimalLength = 9,
91
+ decimalSeparator = '.',
41
92
  } = 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
- // No decimal point, just apply integer length limit
66
- if (maxIntegerLength > 0 && cleaned.length > maxIntegerLength) {
67
- cleaned = cleaned.slice(0, maxIntegerLength);
68
- }
69
- } else {
70
- // Split integer and decimal parts
71
- let integerPart = cleaned.slice(0, firstDot);
72
- let decimalPart = cleaned.slice(firstDot + 1).replace(/\./g, '');
73
93
 
74
- // Apply length limits
75
- if (maxIntegerLength > 0 && integerPart.length > maxIntegerLength) {
76
- integerPart = integerPart.slice(0, maxIntegerLength);
77
- }
78
- decimalPart = decimalPart.slice(0, maxDecimalLength);
94
+ const sanitized = stripLeadingZeros(sanitize(value));
79
95
 
80
- // Ensure integer part is not empty
81
- if (integerPart === '') integerPart = '0';
96
+ const cleaned = allowDecimals
97
+ ? formatDecimal(sanitized, maxIntegerLength, maxDecimalLength)
98
+ : formatIntegerOnly(sanitized, maxIntegerLength);
82
99
 
83
- // Preserve trailing dot if user is typing
84
- const hasTrailingDot =
85
- normalizedValue.endsWith('.') || cleaned.endsWith('.');
86
- cleaned =
87
- decimalPart.length > 0
88
- ? `${integerPart}.${decimalPart}`
89
- : hasTrailingDot
90
- ? `${integerPart}.`
91
- : integerPart;
92
- }
100
+ const grouped = thousandsSeparator ? formatThousands(cleaned) : cleaned;
93
101
 
94
- return thousandsSeparator ? formatThousands(cleaned) : cleaned;
102
+ // formatThousands groups with spaces and keeps a single dot for the decimal,
103
+ // so localizing the separator only ever touches that one dot.
104
+ return decimalSeparator === '.'
105
+ ? grouped
106
+ : grouped.replaceAll('.', decimalSeparator);
95
107
  }
@@ -0,0 +1 @@
1
+ export * from './resolveProps';
@@ -0,0 +1,22 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { createPropsResolver } from './resolveProps';
3
+
4
+ describe('createPropsResolver', () => {
5
+ const resolveProps = createPropsResolver({
6
+ avatar: {
7
+ sm: { size: 'lg', appearance: 'red' },
8
+ md: { size: 'xl', appearance: 'red' },
9
+ },
10
+ });
11
+
12
+ it('resolves the config for a given context and size', () => {
13
+ expect(resolveProps('avatar', 'sm')).toEqual({
14
+ size: 'lg',
15
+ appearance: 'red',
16
+ });
17
+ expect(resolveProps('avatar', 'md')).toEqual({
18
+ size: 'xl',
19
+ appearance: 'red',
20
+ });
21
+ });
22
+ });
@@ -0,0 +1,11 @@
1
+ export type PropsMap<Variant extends string, Props> = Record<Variant, Props>;
2
+ export type ContextPropsMap = Record<string, PropsMap<string, unknown>>;
3
+
4
+ export function createPropsResolver<T extends ContextPropsMap>(config: T) {
5
+ return function resolveProps<
6
+ Context extends keyof T,
7
+ Variant extends keyof T[Context],
8
+ >(context: Context, variant: Variant) {
9
+ return config[context][variant];
10
+ };
11
+ }