@ledgerhq/lumen-utils-shared 0.0.19 → 0.1.0

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 (37) hide show
  1. package/dist/index.d.ts +1 -0
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +143 -102
  4. package/dist/lib/components/amountDisplayHelpers/buildAriaLabel.d.ts +3 -0
  5. package/dist/lib/components/amountDisplayHelpers/buildAriaLabel.d.ts.map +1 -0
  6. package/dist/lib/components/amountDisplayHelpers/index.d.ts +4 -0
  7. package/dist/lib/components/amountDisplayHelpers/index.d.ts.map +1 -0
  8. package/dist/lib/components/amountDisplayHelpers/types.d.ts +30 -0
  9. package/dist/lib/components/amountDisplayHelpers/types.d.ts.map +1 -0
  10. package/dist/lib/components/amountDisplayHelpers/useSplitText.d.ts +6 -0
  11. package/dist/lib/components/amountDisplayHelpers/useSplitText.d.ts.map +1 -0
  12. package/dist/lib/components/getStepperCalculations/getStepperCalculations.d.ts +61 -0
  13. package/dist/lib/components/getStepperCalculations/getStepperCalculations.d.ts.map +1 -0
  14. package/dist/lib/components/getStepperCalculations/index.d.ts +2 -0
  15. package/dist/lib/components/getStepperCalculations/index.d.ts.map +1 -0
  16. package/dist/lib/components/index.d.ts +3 -0
  17. package/dist/lib/components/index.d.ts.map +1 -0
  18. package/dist/lib/createSafeContext/createSafeContext.d.ts.map +1 -1
  19. package/dist/lib/getStepperCalculations/getStepperCalculations.d.ts +61 -0
  20. package/dist/lib/getStepperCalculations/getStepperCalculations.d.ts.map +1 -0
  21. package/dist/lib/getStepperCalculations/index.d.ts +2 -0
  22. package/dist/lib/getStepperCalculations/index.d.ts.map +1 -0
  23. package/package.json +1 -1
  24. package/src/index.ts +1 -0
  25. package/src/lib/components/amountDisplayHelpers/buildAriaLabel.ts +20 -0
  26. package/src/lib/components/amountDisplayHelpers/index.ts +3 -0
  27. package/src/lib/components/amountDisplayHelpers/types.ts +30 -0
  28. package/src/lib/components/amountDisplayHelpers/useSplitText.test.ts +101 -0
  29. package/src/lib/components/amountDisplayHelpers/useSplitText.ts +25 -0
  30. package/src/lib/components/getStepperCalculations/getStepperCalculations.test.ts +63 -0
  31. package/src/lib/components/getStepperCalculations/getStepperCalculations.ts +104 -0
  32. package/src/lib/components/getStepperCalculations/index.ts +5 -0
  33. package/src/lib/components/index.ts +2 -0
  34. package/src/lib/createSafeContext/createSafeContext.tsx +3 -3
  35. package/src/lib/getStepperCalculations/getStepperCalculations.test.ts +139 -0
  36. package/src/lib/getStepperCalculations/getStepperCalculations.ts +103 -0
  37. package/src/lib/getStepperCalculations/index.ts +5 -0
package/dist/index.d.ts CHANGED
@@ -5,6 +5,7 @@ export * from './lib/cn';
5
5
  export * from './lib/isTextChildren';
6
6
  export * from './lib/useMergeRef';
7
7
  export * from './lib/getObjectPath';
8
+ export * from './lib/components';
8
9
  export * from './lib/throttle';
9
10
  export * from './lib/debounce';
10
11
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,qBAAqB,CAAC;AACpC,cAAc,sBAAsB,CAAC;AACrC,cAAc,yBAAyB,CAAC;AACxC,cAAc,UAAU,CAAC;AACzB,cAAc,sBAAsB,CAAC;AACrC,cAAc,mBAAmB,CAAC;AAClC,cAAc,qBAAqB,CAAC;AACpC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,gBAAgB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,qBAAqB,CAAC;AACpC,cAAc,sBAAsB,CAAC;AACrC,cAAc,yBAAyB,CAAC;AACxC,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"}
package/dist/index.js CHANGED
@@ -1,162 +1,203 @@
1
- import { jsx as W } from "react/jsx-runtime";
2
- import { createContext as k, useMemo as A, use as F } from "react";
1
+ import { jsx as k } from "react/jsx-runtime";
2
+ import { createContext as W, useMemo as D, useContext as F } from "react";
3
3
  import { clsx as _ } from "clsx";
4
4
  import { twMerge as j } from "tailwind-merge";
5
5
  function q(t) {
6
- return t ? t.split(/[-_ ]+/).map((n) => n.charAt(0).toUpperCase() + n.slice(1).toLowerCase()).join("") : "";
6
+ return t ? t.split(/[-_ ]+/).map((e) => e.charAt(0).toUpperCase() + e.slice(1).toLowerCase()).join("") : "";
7
7
  }
8
- const z = 48, N = 17, R = 2;
8
+ const N = 48, R = 17, w = 2;
9
9
  function G(t) {
10
- const n = t.replace(/\D/g, "").length;
10
+ const e = t.replace(/\D/g, "").length;
11
11
  return Math.max(
12
- N,
13
- z - n * R
12
+ R,
13
+ N - e * w
14
14
  );
15
15
  }
16
- function D(t, n = !0) {
16
+ function C(t, e = !0) {
17
17
  if (!t) return "";
18
- const l = t.includes("."), [o, r] = t.split("."), c = o.replace(/\B(?=(\d{3})+(?!\d))/g, " ");
19
- return l && n ? r ? `${c}.${r}` : `${c}.` : c;
18
+ const o = t.includes("."), [c, r] = t.split("."), u = c.replace(/\B(?=(\d{3})+(?!\d))/g, " ");
19
+ return o && e ? r ? `${u}.${r}` : `${u}.` : u;
20
20
  }
21
- function H(t, n = {}) {
21
+ function H(t, e = {}) {
22
22
  const {
23
- allowDecimals: l = !0,
24
- thousandsSeparator: o = !0,
23
+ allowDecimals: o = !0,
24
+ thousandsSeparator: c = !0,
25
25
  maxIntegerLength: r = 9,
26
- maxDecimalLength: c = 9
27
- } = n, i = t.replace(",", ".").replace(/[^\d.]/g, "");
28
- let e = i;
29
- if (e = e.replace(/^0+(?=\d)/, ""), !l)
30
- return e = e.replace(/\D/g, ""), r > 0 && e.length > r && (e = e.slice(0, r)), o ? D(e) : e;
31
- e === "." && (e = "0.");
32
- const f = e.indexOf(".");
33
- if (f !== -1) {
34
- let s = e.slice(0, f), a = e.slice(f + 1).replace(/\./g, "");
35
- r > 0 && s.length > r && (s = s.slice(0, r)), a = a.slice(0, c), s === "" && (s = "0");
36
- const g = i.endsWith(".") || e.endsWith(".");
37
- e = a.length > 0 ? `${s}.${a}` : g ? `${s}.` : s;
26
+ maxDecimalLength: u = 9
27
+ } = e, i = t.replace(",", ".").replace(/[^\d.]/g, "");
28
+ let n = i;
29
+ if (n = n.replace(/^0+(?=\d)/, ""), !o)
30
+ return n = n.replace(/\D/g, ""), r > 0 && n.length > r && (n = n.slice(0, r)), c ? C(n) : n;
31
+ n === "." && (n = "0.");
32
+ const a = n.indexOf(".");
33
+ if (a !== -1) {
34
+ let s = n.slice(0, a), f = n.slice(a + 1).replace(/\./g, "");
35
+ r > 0 && s.length > r && (s = s.slice(0, r)), f = f.slice(0, u), s === "" && (s = "0");
36
+ const h = i.endsWith(".") || n.endsWith(".");
37
+ n = f.length > 0 ? `${s}.${f}` : h ? `${s}.` : s;
38
38
  } else
39
- r > 0 && e.length > r && (e = e.slice(0, r));
40
- return o ? D(e) : e;
39
+ r > 0 && n.length > r && (n = n.slice(0, r));
40
+ return c ? C(n) : n;
41
41
  }
42
- function J(t, n) {
43
- const l = k(n), o = ({ children: c, value: i }) => {
44
- const e = A(
42
+ function J(t, e) {
43
+ const o = W(e), c = ({ children: u, value: i }) => {
44
+ const n = D(
45
45
  () => i,
46
46
  Object.values(i ?? {})
47
47
  );
48
- return /* @__PURE__ */ W(l, { value: e, children: c });
48
+ return /* @__PURE__ */ k(o.Provider, { value: n, children: u });
49
49
  };
50
- o.displayName = t + "Provider";
50
+ c.displayName = t + "Provider";
51
51
  function r({
52
- consumerName: c,
52
+ consumerName: u,
53
53
  contextRequired: i
54
54
  }) {
55
- const e = F(l);
56
- if (e)
57
- return e;
55
+ const n = F(o);
56
+ if (n)
57
+ return n;
58
58
  if (i)
59
59
  throw new Error(
60
- `${c} must be used within ${t}`
60
+ `${u} must be used within ${t}`
61
61
  );
62
- return n || {};
62
+ return e || {};
63
63
  }
64
- return [o, r];
64
+ return [c, r];
65
65
  }
66
66
  function K(...t) {
67
67
  return j(_(t));
68
68
  }
69
- const I = (t) => typeof t == "string" || typeof t == "number", Q = (t) => Array.isArray(t) ? t.every(I) : I(t);
70
- function M(t, n) {
69
+ const S = (t) => typeof t == "string" || typeof t == "number", Q = (t) => Array.isArray(t) ? t.every(S) : S(t);
70
+ function I(t, e) {
71
71
  if (typeof t == "function")
72
- return t(n);
73
- typeof t == "object" && t !== null && "current" in t && (t.current = n);
72
+ return t(e);
73
+ typeof t == "object" && t !== null && "current" in t && (t.current = e);
74
74
  }
75
- function w(...t) {
76
- const n = /* @__PURE__ */ new Map();
77
- return (l) => {
78
- if (l === null) {
79
- t.forEach((o) => {
80
- const r = n.get(o);
81
- typeof r == "function" && r(), M(o, null);
82
- }), n.clear();
75
+ function z(...t) {
76
+ const e = /* @__PURE__ */ new Map();
77
+ return (o) => {
78
+ if (o === null) {
79
+ t.forEach((c) => {
80
+ const r = e.get(c);
81
+ typeof r == "function" && r(), I(c, null);
82
+ }), e.clear();
83
83
  return;
84
84
  }
85
- t.forEach((o) => {
86
- const r = M(o, l);
87
- typeof r == "function" && n.set(o, r);
85
+ t.forEach((c) => {
86
+ const r = I(c, o);
87
+ typeof r == "function" && e.set(c, r);
88
88
  });
89
89
  };
90
90
  }
91
91
  function Y(...t) {
92
- return A(() => w(...t), t);
92
+ return D(() => z(...t), t);
93
93
  }
94
- const tt = (t, n) => {
95
- let l = t;
96
- for (const o of n) {
97
- if (l == null)
94
+ const tt = (t, e) => {
95
+ let o = t;
96
+ for (const c of e) {
97
+ if (o == null)
98
98
  return;
99
- l = l[o];
99
+ o = o[c];
100
100
  }
101
- return l;
101
+ return o;
102
+ }, et = ({
103
+ currentStep: t,
104
+ totalSteps: e,
105
+ size: o,
106
+ label: c,
107
+ strokeWidth: r = 4,
108
+ arcPercentage: u = 0.75
109
+ }) => {
110
+ const i = Math.min(Math.max(t, 0), e), n = c ?? `${i}/${e}`, a = e <= 0 ? 0 : i / e, s = (o - r) / 2, f = o / 2, h = o / 2, m = 2 * Math.PI * s, g = m * u, T = g * (1 - a), p = i <= 0, A = `${g} ${m}`, $ = p ? g - 2 : T;
111
+ return {
112
+ displayLabel: n,
113
+ progress: a,
114
+ r: s,
115
+ cx: f,
116
+ cy: h,
117
+ circumference: m,
118
+ trackArcLength: g,
119
+ trackDashArray: `${g} ${m}`,
120
+ progressDashArray: A,
121
+ progressDashOffset: $,
122
+ showMinimalDot: p
123
+ };
124
+ }, nt = (t) => D(
125
+ () => ({
126
+ integerPart: Array.from(t.integerPart, (e) => ({
127
+ value: e,
128
+ type: e.match(/^\d$/) ? "digit" : "separator"
129
+ })),
130
+ decimalPart: t.decimalPart ? Array.from(t.decimalPart, (e) => ({
131
+ value: e,
132
+ type: "digit"
133
+ })) : []
134
+ }),
135
+ [t.integerPart, t.decimalPart]
136
+ ), rt = (t, e, o) => {
137
+ if (e) return o;
138
+ const c = t.decimalPart ? `${t.decimalSeparator}${t.decimalPart}` : "", r = `${t.integerPart}${c}`;
139
+ return t.currencyPosition === "end" ? `${r} ${t.currencyText}` : `${t.currencyText} ${r}`;
102
140
  };
103
- function V(t, n, l = {}) {
104
- const { leading: o = !1, trailing: r = !0, maxWait: c } = l;
105
- let i = null, e = null, f = null, s = 0, a = null, g = null, d;
106
- const x = c !== void 0, S = x ? Math.max(c, n) : 0, T = (u) => {
107
- s = u;
108
- const m = a, h = g;
109
- return a = null, g = null, d = t.apply(h, m), d;
110
- }, E = (u) => {
111
- const m = f ? u - f : 0;
112
- return n - m;
113
- }, v = (u) => {
114
- const m = f ? u - f : n, h = u - s;
115
- return f === null || m >= n || m < 0 || x && h >= S;
116
- }, C = (u) => (i = null, r && a ? T(u) : (a = null, g = null, d)), p = () => {
117
- const u = Date.now();
118
- if (v(u)) {
119
- C(u);
141
+ function V(t, e, o = {}) {
142
+ const { leading: c = !1, trailing: r = !0, maxWait: u } = o;
143
+ let i = null, n = null, a = null, s = 0, f = null, h = null, m;
144
+ const g = u !== void 0, T = g ? Math.max(u, e) : 0, p = (l) => {
145
+ s = l;
146
+ const d = f, y = h;
147
+ return f = null, h = null, m = t.apply(y, d), m;
148
+ }, A = (l) => {
149
+ const d = a ? l - a : 0;
150
+ return e - d;
151
+ }, $ = (l) => {
152
+ const d = a ? l - a : e, y = l - s;
153
+ return a === null || d >= e || d < 0 || g && y >= T;
154
+ }, M = (l) => (i = null, r && f ? p(l) : (f = null, h = null, m)), x = () => {
155
+ const l = Date.now();
156
+ if ($(l)) {
157
+ M(l);
120
158
  return;
121
159
  }
122
- i = setTimeout(p, E(u));
123
- }, P = () => {
124
- const u = Date.now();
125
- e = null, a && (T(u), i !== null && (clearTimeout(i), i = setTimeout(p, n)));
126
- }, b = (u) => (s = u, i = setTimeout(p, n), x && (e = setTimeout(P, S)), o ? T(u) : d), $ = () => {
127
- i !== null && clearTimeout(i), e !== null && clearTimeout(e), s = 0, a = null, g = null, f = null, i = null, e = null;
128
- }, L = () => i === null && e === null ? d : C(Date.now()), O = () => i !== null || e !== null, y = function(...u) {
129
- const m = Date.now(), h = v(m);
130
- if (a = u, g = this, f = m, h) {
160
+ i = setTimeout(x, A(l));
161
+ }, b = () => {
162
+ const l = Date.now();
163
+ n = null, f && (p(l), i !== null && (clearTimeout(i), i = setTimeout(x, e)));
164
+ }, v = (l) => (s = l, i = setTimeout(x, e), g && (n = setTimeout(b, T)), c ? p(l) : m), E = () => {
165
+ i !== null && clearTimeout(i), n !== null && clearTimeout(n), s = 0, f = null, h = null, a = null, i = null, n = null;
166
+ }, L = () => i === null && n === null ? m : M(Date.now()), O = () => i !== null || n !== null, P = function(...l) {
167
+ const d = Date.now(), y = $(d);
168
+ if (f = l, h = this, a = d, y) {
131
169
  if (i === null)
132
- return b(m);
133
- if (x)
134
- return clearTimeout(i), i = setTimeout(p, n), T(m);
170
+ return v(d);
171
+ if (g)
172
+ return clearTimeout(i), i = setTimeout(x, e), p(d);
135
173
  }
136
- return i === null && (i = setTimeout(p, n)), d;
174
+ return i === null && (i = setTimeout(x, e)), m;
137
175
  };
138
- return y.cancel = $, y.flush = L, y.pending = O, y;
176
+ return P.cancel = E, P.flush = L, P.pending = O, P;
139
177
  }
140
- function et(t, n, l = {}) {
141
- const { leading: o = !0, trailing: r = !0 } = l;
142
- return V(t, n, {
143
- leading: o,
178
+ function it(t, e, o = {}) {
179
+ const { leading: c = !0, trailing: r = !0 } = o;
180
+ return V(t, e, {
181
+ leading: c,
144
182
  trailing: r,
145
- maxWait: n
183
+ maxWait: e
146
184
  });
147
185
  }
148
186
  export {
149
- M as assignRef,
187
+ I as assignRef,
188
+ rt as buildAriaLabel,
150
189
  K as cn,
151
190
  J as createSafeContext,
152
191
  V as debounce,
153
- D as formatThousands,
192
+ C as formatThousands,
154
193
  G as getFontSize,
155
194
  tt as getObjectPath,
195
+ et as getStepperCalculations,
156
196
  Q as isTextChildren,
157
- w as mergeRefs,
197
+ z as mergeRefs,
158
198
  H as textFormatter,
159
- et as throttle,
199
+ it as throttle,
160
200
  q as toPascalCase,
161
- Y as useMergedRef
201
+ Y as useMergedRef,
202
+ nt as useSplitText
162
203
  };
@@ -0,0 +1,3 @@
1
+ import { FormattedValue } from './types';
2
+ export declare const buildAriaLabel: (parts: FormattedValue, hidden: boolean, hiddenLabel: string) => string;
3
+ //# sourceMappingURL=buildAriaLabel.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"buildAriaLabel.d.ts","sourceRoot":"","sources":["../../../../src/lib/components/amountDisplayHelpers/buildAriaLabel.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAEzC,eAAO,MAAM,cAAc,UAClB,cAAc,UACb,OAAO,eACF,MAAM,KAClB,MAaF,CAAC"}
@@ -0,0 +1,4 @@
1
+ export { useSplitText } from './useSplitText';
2
+ export { buildAriaLabel } from './buildAriaLabel';
3
+ export type { FormattedValue, SplitChar } from './types';
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/lib/components/amountDisplayHelpers/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC9C,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAClD,YAAY,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC"}
@@ -0,0 +1,30 @@
1
+ export type SplitChar = {
2
+ value: string;
3
+ type: 'digit' | 'separator';
4
+ };
5
+ export type FormattedValue = {
6
+ /**
7
+ * The whole number portion of the amount (e.g., "1234" from 1234.56)
8
+ */
9
+ integerPart: string;
10
+ /**
11
+ * The fractional portion of the amount without the separator (e.g., "56" from 1234.56)
12
+ * @optional
13
+ */
14
+ decimalPart?: string;
15
+ /**
16
+ * The currency text or symbol (e.g., "$", "USD", "€", "BTC")
17
+ */
18
+ currencyText: string;
19
+ /**
20
+ * The character which separates integer and fractional parts.
21
+ */
22
+ decimalSeparator: '.' | ',';
23
+ /**
24
+ * Position of the currency text relative to the amount.
25
+ * @optional
26
+ * @default 'start'
27
+ */
28
+ currencyPosition?: 'start' | 'end';
29
+ };
30
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../../src/lib/components/amountDisplayHelpers/types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,SAAS,GAAG;IACtB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,OAAO,GAAG,WAAW,CAAC;CAC7B,CAAC;AAEF,MAAM,MAAM,cAAc,GAAG;IAC3B;;OAEG;IACH,WAAW,EAAE,MAAM,CAAC;IACpB;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;OAEG;IACH,YAAY,EAAE,MAAM,CAAC;IACrB;;OAEG;IACH,gBAAgB,EAAE,GAAG,GAAG,GAAG,CAAC;IAC5B;;;;OAIG;IACH,gBAAgB,CAAC,EAAE,OAAO,GAAG,KAAK,CAAC;CACpC,CAAC"}
@@ -0,0 +1,6 @@
1
+ import { FormattedValue, SplitChar } from './types';
2
+ export declare const useSplitText: (value: FormattedValue) => {
3
+ integerPart: SplitChar[];
4
+ decimalPart: SplitChar[];
5
+ };
6
+ //# sourceMappingURL=useSplitText.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useSplitText.d.ts","sourceRoot":"","sources":["../../../../src/lib/components/amountDisplayHelpers/useSplitText.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AAEpD,eAAO,MAAM,YAAY,UAChB,cAAc,KACpB;IACD,WAAW,EAAE,SAAS,EAAE,CAAC;IACzB,WAAW,EAAE,SAAS,EAAE,CAAC;CAiB1B,CAAC"}
@@ -0,0 +1,61 @@
1
+ export type StepperCalculationsInput = {
2
+ /** Current step number (1-based). Use 0 or negative for minimal dot. */
3
+ currentStep: number;
4
+ /** Total number of steps. */
5
+ totalSteps: number;
6
+ /** Size of the circular stepper in pixels. */
7
+ size: number;
8
+ /** Optional custom label. Defaults to "{currentStep}/{totalSteps}". */
9
+ label?: string;
10
+ /** Stroke width in pixels. @default 4 */
11
+ strokeWidth?: number;
12
+ /** Percentage of full circle to show as arc (0–1). @default 0.75 (270°) */
13
+ arcPercentage?: number;
14
+ };
15
+ export type StepperCalculationsOutput = {
16
+ /** Label text displayed in the center. */
17
+ displayLabel: string;
18
+ /** Progress value between 0 and 1. */
19
+ progress: number;
20
+ /** Circle radius in pixels. */
21
+ r: number;
22
+ /** Center x coordinate. */
23
+ cx: number;
24
+ /** Center y coordinate. */
25
+ cy: number;
26
+ /** Full circle circumference. */
27
+ circumference: number;
28
+ /** Length of the visible arc (track). */
29
+ trackArcLength: number;
30
+ /** strokeDasharray for the gray track circle. */
31
+ trackDashArray: string;
32
+ /** strokeDasharray for the progress (purple) circle. */
33
+ progressDashArray: string;
34
+ /** strokeDashoffset for the progress (purple) circle. */
35
+ progressDashOffset: number;
36
+ /** Whether to show minimal dot instead of progress arc. */
37
+ showMinimalDot: boolean;
38
+ };
39
+ /**
40
+ * Computes all SVG-related values needed to render a circular stepper.
41
+ *
42
+ * This is a pure utility shared between React (web) and React Native
43
+ * implementations of the Stepper component.
44
+ *
45
+ * @example
46
+ * ```ts
47
+ * const calcs = getStepperCalculations({
48
+ * currentStep: 2,
49
+ * totalSteps: 4,
50
+ * size: 48,
51
+ * });
52
+ *
53
+ * // calcs.displayLabel → '2/4'
54
+ * // calcs.progress → 0.5
55
+ * // calcs.trackDashArray → '<trackArcLength> <circumference>'
56
+ * // calcs.progressDashArray → '<trackArcLength> <circumference>'
57
+ * // calcs.progressDashOffset → offset for 50% fill
58
+ * ```
59
+ */
60
+ export declare const getStepperCalculations: ({ currentStep, totalSteps, size, label, strokeWidth, arcPercentage, }: StepperCalculationsInput) => StepperCalculationsOutput;
61
+ //# sourceMappingURL=getStepperCalculations.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"getStepperCalculations.d.ts","sourceRoot":"","sources":["../../../../src/lib/components/getStepperCalculations/getStepperCalculations.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,wBAAwB,GAAG;IACrC,wEAAwE;IACxE,WAAW,EAAE,MAAM,CAAC;IACpB,6BAA6B;IAC7B,UAAU,EAAE,MAAM,CAAC;IACnB,8CAA8C;IAC9C,IAAI,EAAE,MAAM,CAAC;IACb,uEAAuE;IACvE,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,yCAAyC;IACzC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,2EAA2E;IAC3E,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB,CAAC;AAEF,MAAM,MAAM,yBAAyB,GAAG;IACtC,0CAA0C;IAC1C,YAAY,EAAE,MAAM,CAAC;IACrB,sCAAsC;IACtC,QAAQ,EAAE,MAAM,CAAC;IACjB,+BAA+B;IAC/B,CAAC,EAAE,MAAM,CAAC;IACV,2BAA2B;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,2BAA2B;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,iCAAiC;IACjC,aAAa,EAAE,MAAM,CAAC;IACtB,yCAAyC;IACzC,cAAc,EAAE,MAAM,CAAC;IACvB,iDAAiD;IACjD,cAAc,EAAE,MAAM,CAAC;IACvB,wDAAwD;IACxD,iBAAiB,EAAE,MAAM,CAAC;IAC1B,yDAAyD;IACzD,kBAAkB,EAAE,MAAM,CAAC;IAC3B,2DAA2D;IAC3D,cAAc,EAAE,OAAO,CAAC;CACzB,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,eAAO,MAAM,sBAAsB,0EAOhC,wBAAwB,KAAG,yBAmC7B,CAAC"}
@@ -0,0 +1,2 @@
1
+ export { getStepperCalculations, type StepperCalculationsInput, type StepperCalculationsOutput, } from './getStepperCalculations';
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/lib/components/getStepperCalculations/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,sBAAsB,EACtB,KAAK,wBAAwB,EAC7B,KAAK,yBAAyB,GAC/B,MAAM,0BAA0B,CAAC"}
@@ -0,0 +1,3 @@
1
+ export * from './getStepperCalculations';
2
+ export * from './amountDisplayHelpers';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/lib/components/index.ts"],"names":[],"mappings":"AAAA,cAAc,0BAA0B,CAAC;AACzC,cAAc,wBAAwB,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"createSafeContext.d.ts","sourceRoot":"","sources":["../../../src/lib/createSafeContext/createSafeContext.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAiB,EAAE,EAAE,SAAS,EAAgB,MAAM,OAAO,CAAC;AAEnE,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"}
1
+ {"version":3,"file":"createSafeContext.d.ts","sourceRoot":"","sources":["../../../src/lib/createSafeContext/createSafeContext.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,61 @@
1
+ export type StepperCalculationsInput = {
2
+ /** Current step number (1-based). Use 0 or negative for minimal dot. */
3
+ currentStep: number;
4
+ /** Total number of steps. */
5
+ totalSteps: number;
6
+ /** Size of the circular stepper in pixels. */
7
+ size: number;
8
+ /** Optional custom label. Defaults to "{currentStep}/{totalSteps}". */
9
+ label?: string;
10
+ /** Stroke width in pixels. @default 4 */
11
+ strokeWidth?: number;
12
+ /** Percentage of full circle to show as arc (0–1). @default 0.75 (270°) */
13
+ arcPercentage?: number;
14
+ };
15
+ export type StepperCalculationsOutput = {
16
+ /** Label text displayed in the center. */
17
+ displayLabel: string;
18
+ /** Progress value between 0 and 1. */
19
+ progress: number;
20
+ /** Circle radius in pixels. */
21
+ r: number;
22
+ /** Center x coordinate. */
23
+ cx: number;
24
+ /** Center y coordinate. */
25
+ cy: number;
26
+ /** Full circle circumference. */
27
+ circumference: number;
28
+ /** Length of the visible arc (track). */
29
+ trackArcLength: number;
30
+ /** strokeDasharray for the gray track circle. */
31
+ trackDashArray: string;
32
+ /** strokeDasharray for the progress (purple) circle. */
33
+ progressDashArray: string;
34
+ /** strokeDashoffset for the progress (purple) circle. */
35
+ progressDashOffset: number;
36
+ /** Whether to show minimal dot instead of progress arc. */
37
+ showMinimalDot: boolean;
38
+ };
39
+ /**
40
+ * Computes all SVG-related values needed to render a circular stepper.
41
+ *
42
+ * This is a pure utility shared between React (web) and React Native
43
+ * implementations of the Stepper component.
44
+ *
45
+ * @example
46
+ * ```ts
47
+ * const calcs = getStepperCalculations({
48
+ * currentStep: 2,
49
+ * totalSteps: 4,
50
+ * size: 48,
51
+ * });
52
+ *
53
+ * // calcs.displayLabel → '2/4'
54
+ * // calcs.progress → 0.5
55
+ * // calcs.trackDashArray → '<trackArcLength> <circumference>'
56
+ * // calcs.progressDashArray → '<trackArcLength> <circumference>'
57
+ * // calcs.progressDashOffset → offset for 50% fill
58
+ * ```
59
+ */
60
+ export declare const getStepperCalculations: ({ currentStep, totalSteps, size, label, strokeWidth, arcPercentage, }: StepperCalculationsInput) => StepperCalculationsOutput;
61
+ //# sourceMappingURL=getStepperCalculations.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"getStepperCalculations.d.ts","sourceRoot":"","sources":["../../../src/lib/getStepperCalculations/getStepperCalculations.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,wBAAwB,GAAG;IACrC,wEAAwE;IACxE,WAAW,EAAE,MAAM,CAAC;IACpB,6BAA6B;IAC7B,UAAU,EAAE,MAAM,CAAC;IACnB,8CAA8C;IAC9C,IAAI,EAAE,MAAM,CAAC;IACb,uEAAuE;IACvE,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,yCAAyC;IACzC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,2EAA2E;IAC3E,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB,CAAC;AAEF,MAAM,MAAM,yBAAyB,GAAG;IACtC,0CAA0C;IAC1C,YAAY,EAAE,MAAM,CAAC;IACrB,sCAAsC;IACtC,QAAQ,EAAE,MAAM,CAAC;IACjB,+BAA+B;IAC/B,CAAC,EAAE,MAAM,CAAC;IACV,2BAA2B;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,2BAA2B;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,iCAAiC;IACjC,aAAa,EAAE,MAAM,CAAC;IACtB,yCAAyC;IACzC,cAAc,EAAE,MAAM,CAAC;IACvB,iDAAiD;IACjD,cAAc,EAAE,MAAM,CAAC;IACvB,wDAAwD;IACxD,iBAAiB,EAAE,MAAM,CAAC;IAC1B,yDAAyD;IACzD,kBAAkB,EAAE,MAAM,CAAC;IAC3B,2DAA2D;IAC3D,cAAc,EAAE,OAAO,CAAC;CACzB,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,eAAO,MAAM,sBAAsB,0EAOhC,wBAAwB,KAAG,yBAkC7B,CAAC"}
@@ -0,0 +1,2 @@
1
+ export { getStepperCalculations, type StepperCalculationsInput, type StepperCalculationsOutput, } from './getStepperCalculations';
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/lib/getStepperCalculations/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,sBAAsB,EACtB,KAAK,wBAAwB,EAC7B,KAAK,yBAAyB,GAC/B,MAAM,0BAA0B,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ledgerhq/lumen-utils-shared",
3
- "version": "0.0.19",
3
+ "version": "0.1.0",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.js",
package/src/index.ts CHANGED
@@ -5,5 +5,6 @@ export * from './lib/cn';
5
5
  export * from './lib/isTextChildren';
6
6
  export * from './lib/useMergeRef';
7
7
  export * from './lib/getObjectPath';
8
+ export * from './lib/components';
8
9
  export * from './lib/throttle';
9
10
  export * from './lib/debounce';
@@ -0,0 +1,20 @@
1
+ import { FormattedValue } from './types';
2
+
3
+ export const buildAriaLabel = (
4
+ parts: FormattedValue,
5
+ hidden: boolean,
6
+ hiddenLabel: string,
7
+ ): string => {
8
+ if (hidden) return hiddenLabel;
9
+
10
+ const decimal = parts.decimalPart
11
+ ? `${parts.decimalSeparator}${parts.decimalPart}`
12
+ : '';
13
+ const amount = `${parts.integerPart}${decimal}`;
14
+
15
+ if (parts.currencyPosition === 'end') {
16
+ return `${amount} ${parts.currencyText}`;
17
+ }
18
+
19
+ return `${parts.currencyText} ${amount}`;
20
+ };
@@ -0,0 +1,3 @@
1
+ export { useSplitText } from './useSplitText';
2
+ export { buildAriaLabel } from './buildAriaLabel';
3
+ export type { FormattedValue, SplitChar } from './types';
@@ -0,0 +1,30 @@
1
+ export type SplitChar = {
2
+ value: string;
3
+ type: 'digit' | 'separator';
4
+ };
5
+
6
+ export type FormattedValue = {
7
+ /**
8
+ * The whole number portion of the amount (e.g., "1234" from 1234.56)
9
+ */
10
+ integerPart: string;
11
+ /**
12
+ * The fractional portion of the amount without the separator (e.g., "56" from 1234.56)
13
+ * @optional
14
+ */
15
+ decimalPart?: string;
16
+ /**
17
+ * The currency text or symbol (e.g., "$", "USD", "€", "BTC")
18
+ */
19
+ currencyText: string;
20
+ /**
21
+ * The character which separates integer and fractional parts.
22
+ */
23
+ decimalSeparator: '.' | ',';
24
+ /**
25
+ * Position of the currency text relative to the amount.
26
+ * @optional
27
+ * @default 'start'
28
+ */
29
+ currencyPosition?: 'start' | 'end';
30
+ };
@@ -0,0 +1,101 @@
1
+ import { renderHook } from '@testing-library/react';
2
+ import { describe, it, expect } from 'vitest';
3
+
4
+ import { FormattedValue } from './types';
5
+ import { useSplitText } from './useSplitText';
6
+
7
+ const createValue = (
8
+ overrides: Partial<FormattedValue> = {},
9
+ ): FormattedValue => ({
10
+ integerPart: '1234',
11
+ decimalPart: '56',
12
+ currencyText: 'USD',
13
+ decimalSeparator: '.',
14
+ currencyPosition: 'start',
15
+ ...overrides,
16
+ });
17
+
18
+ describe('useSplitText', () => {
19
+ it('splits integer part into individual digit characters', () => {
20
+ const { result } = renderHook(() => useSplitText(createValue()));
21
+
22
+ expect(result.current.integerPart).toEqual([
23
+ { value: '1', type: 'digit' },
24
+ { value: '2', type: 'digit' },
25
+ { value: '3', type: 'digit' },
26
+ { value: '4', type: 'digit' },
27
+ ]);
28
+ });
29
+
30
+ it('splits decimal part into individual digit characters', () => {
31
+ const { result } = renderHook(() => useSplitText(createValue()));
32
+
33
+ expect(result.current.decimalPart).toEqual([
34
+ { value: '5', type: 'digit' },
35
+ { value: '6', type: 'digit' },
36
+ ]);
37
+ });
38
+
39
+ it('returns empty decimal part when decimalPart is undefined', () => {
40
+ const { result } = renderHook(() =>
41
+ useSplitText(createValue({ decimalPart: undefined })),
42
+ );
43
+
44
+ expect(result.current.decimalPart).toEqual([]);
45
+ });
46
+
47
+ it('classifies group separators in integer part', () => {
48
+ const { result } = renderHook(() =>
49
+ useSplitText(createValue({ integerPart: '1,234,567' })),
50
+ );
51
+
52
+ expect(result.current.integerPart).toEqual([
53
+ { value: '1', type: 'digit' },
54
+ { value: ',', type: 'separator' },
55
+ { value: '2', type: 'digit' },
56
+ { value: '3', type: 'digit' },
57
+ { value: '4', type: 'digit' },
58
+ { value: ',', type: 'separator' },
59
+ { value: '5', type: 'digit' },
60
+ { value: '6', type: 'digit' },
61
+ { value: '7', type: 'digit' },
62
+ ]);
63
+ });
64
+
65
+ it('classifies space separators in integer part', () => {
66
+ const { result } = renderHook(() =>
67
+ useSplitText(createValue({ integerPart: '1 234 567' })),
68
+ );
69
+
70
+ expect(result.current.integerPart).toEqual([
71
+ { value: '1', type: 'digit' },
72
+ { value: ' ', type: 'separator' },
73
+ { value: '2', type: 'digit' },
74
+ { value: '3', type: 'digit' },
75
+ { value: '4', type: 'digit' },
76
+ { value: ' ', type: 'separator' },
77
+ { value: '5', type: 'digit' },
78
+ { value: '6', type: 'digit' },
79
+ { value: '7', type: 'digit' },
80
+ ]);
81
+ });
82
+
83
+ it('handles single digit integer part', () => {
84
+ const { result } = renderHook(() =>
85
+ useSplitText(createValue({ integerPart: '0' })),
86
+ );
87
+
88
+ expect(result.current.integerPart).toEqual([{ value: '0', type: 'digit' }]);
89
+ });
90
+
91
+ it('handles long decimal part', () => {
92
+ const { result } = renderHook(() =>
93
+ useSplitText(createValue({ decimalPart: '00123456' })),
94
+ );
95
+
96
+ expect(result.current.decimalPart).toHaveLength(8);
97
+ expect(result.current.decimalPart.every((c) => c.type === 'digit')).toBe(
98
+ true,
99
+ );
100
+ });
101
+ });
@@ -0,0 +1,25 @@
1
+ import { useMemo } from 'react';
2
+ import { FormattedValue, SplitChar } from './types';
3
+
4
+ export const useSplitText = (
5
+ value: FormattedValue,
6
+ ): {
7
+ integerPart: SplitChar[];
8
+ decimalPart: SplitChar[];
9
+ } => {
10
+ return useMemo(
11
+ () => ({
12
+ integerPart: Array.from(value.integerPart, (digit) => ({
13
+ value: digit,
14
+ type: digit.match(/^\d$/) ? 'digit' : 'separator',
15
+ })),
16
+ decimalPart: value.decimalPart
17
+ ? Array.from(value.decimalPart, (digit) => ({
18
+ value: digit,
19
+ type: 'digit',
20
+ }))
21
+ : [],
22
+ }),
23
+ [value.integerPart, value.decimalPart],
24
+ );
25
+ };
@@ -0,0 +1,63 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { getStepperCalculations } from '../getStepperCalculations';
3
+
4
+ describe('getStepperCalculations', () => {
5
+ const defaultInput = {
6
+ currentStep: 2,
7
+ totalSteps: 4,
8
+ size: 48,
9
+ };
10
+
11
+ it('should compute display label as "currentStep/totalSteps"', () => {
12
+ const result = getStepperCalculations(defaultInput);
13
+ expect(result.displayLabel).toBe('2/4');
14
+ });
15
+
16
+ it('should use custom label when provided', () => {
17
+ const result = getStepperCalculations({
18
+ ...defaultInput,
19
+ label: 'Step 2 of 4',
20
+ });
21
+ expect(result.displayLabel).toBe('Step 2 of 4');
22
+ });
23
+
24
+ it('should compute progress as currentStep / totalSteps', () => {
25
+ const result = getStepperCalculations(defaultInput);
26
+ expect(result.progress).toBe(0.5);
27
+ });
28
+
29
+ it('should clamp progress and label when currentStep is out of bounds', () => {
30
+ const negative = getStepperCalculations({
31
+ ...defaultInput,
32
+ currentStep: -3,
33
+ });
34
+ expect(negative.progress).toBe(0);
35
+ expect(negative.displayLabel).toBe('0/4');
36
+
37
+ const overflow = getStepperCalculations({
38
+ ...defaultInput,
39
+ currentStep: 10,
40
+ });
41
+ expect(overflow.progress).toBe(1);
42
+ expect(overflow.displayLabel).toBe('4/4');
43
+ });
44
+
45
+ it('should handle zero totalSteps gracefully', () => {
46
+ const result = getStepperCalculations({
47
+ ...defaultInput,
48
+ totalSteps: 0,
49
+ });
50
+ expect(result.progress).toBe(0);
51
+ });
52
+
53
+ it('should show minimal dot when currentStep <= 0', () => {
54
+ const zero = getStepperCalculations({
55
+ ...defaultInput,
56
+ currentStep: 0,
57
+ });
58
+ expect(zero.showMinimalDot).toBe(true);
59
+
60
+ const started = getStepperCalculations(defaultInput);
61
+ expect(started.showMinimalDot).toBe(false);
62
+ });
63
+ });
@@ -0,0 +1,104 @@
1
+ export type StepperCalculationsInput = {
2
+ /** Current step number (1-based). Use 0 or negative for minimal dot. */
3
+ currentStep: number;
4
+ /** Total number of steps. */
5
+ totalSteps: number;
6
+ /** Size of the circular stepper in pixels. */
7
+ size: number;
8
+ /** Optional custom label. Defaults to "{currentStep}/{totalSteps}". */
9
+ label?: string;
10
+ /** Stroke width in pixels. @default 4 */
11
+ strokeWidth?: number;
12
+ /** Percentage of full circle to show as arc (0–1). @default 0.75 (270°) */
13
+ arcPercentage?: number;
14
+ };
15
+
16
+ export type StepperCalculationsOutput = {
17
+ /** Label text displayed in the center. */
18
+ displayLabel: string;
19
+ /** Progress value between 0 and 1. */
20
+ progress: number;
21
+ /** Circle radius in pixels. */
22
+ r: number;
23
+ /** Center x coordinate. */
24
+ cx: number;
25
+ /** Center y coordinate. */
26
+ cy: number;
27
+ /** Full circle circumference. */
28
+ circumference: number;
29
+ /** Length of the visible arc (track). */
30
+ trackArcLength: number;
31
+ /** strokeDasharray for the gray track circle. */
32
+ trackDashArray: string;
33
+ /** strokeDasharray for the progress (purple) circle. */
34
+ progressDashArray: string;
35
+ /** strokeDashoffset for the progress (purple) circle. */
36
+ progressDashOffset: number;
37
+ /** Whether to show minimal dot instead of progress arc. */
38
+ showMinimalDot: boolean;
39
+ };
40
+
41
+ /**
42
+ * Computes all SVG-related values needed to render a circular stepper.
43
+ *
44
+ * This is a pure utility shared between React (web) and React Native
45
+ * implementations of the Stepper component.
46
+ *
47
+ * @example
48
+ * ```ts
49
+ * const calcs = getStepperCalculations({
50
+ * currentStep: 2,
51
+ * totalSteps: 4,
52
+ * size: 48,
53
+ * });
54
+ *
55
+ * // calcs.displayLabel → '2/4'
56
+ * // calcs.progress → 0.5
57
+ * // calcs.trackDashArray → '<trackArcLength> <circumference>'
58
+ * // calcs.progressDashArray → '<trackArcLength> <circumference>'
59
+ * // calcs.progressDashOffset → offset for 50% fill
60
+ * ```
61
+ */
62
+ export const getStepperCalculations = ({
63
+ currentStep,
64
+ totalSteps,
65
+ size,
66
+ label,
67
+ strokeWidth = 4,
68
+ arcPercentage = 0.75,
69
+ }: StepperCalculationsInput): StepperCalculationsOutput => {
70
+ // Clamp currentStep: minimum 0, maximum totalSteps
71
+ const clampedCurrentStep = Math.min(Math.max(currentStep, 0), totalSteps);
72
+
73
+ const displayLabel = label ?? `${clampedCurrentStep}/${totalSteps}`;
74
+ const progress = totalSteps <= 0 ? 0 : clampedCurrentStep / totalSteps;
75
+
76
+ // SVG circle geometry
77
+ const r = (size - strokeWidth) / 2;
78
+ const cx = size / 2;
79
+ const cy = size / 2;
80
+ const circumference = 2 * Math.PI * r;
81
+
82
+ // Arc calculations
83
+ const trackArcLength = circumference * arcPercentage;
84
+ const dashOffset = trackArcLength * (1 - progress);
85
+
86
+ // Minimal dot handling (currentStep <= 0 means "not started")
87
+ const showMinimalDot = clampedCurrentStep <= 0;
88
+ const progressDashArray = `${trackArcLength} ${circumference}`;
89
+ const progressDashOffset = showMinimalDot ? trackArcLength - 2 : dashOffset;
90
+
91
+ return {
92
+ displayLabel,
93
+ progress,
94
+ r,
95
+ cx,
96
+ cy,
97
+ circumference,
98
+ trackArcLength,
99
+ trackDashArray: `${trackArcLength} ${circumference}`,
100
+ progressDashArray,
101
+ progressDashOffset,
102
+ showMinimalDot,
103
+ };
104
+ };
@@ -0,0 +1,5 @@
1
+ export {
2
+ getStepperCalculations,
3
+ type StepperCalculationsInput,
4
+ type StepperCalculationsOutput,
5
+ } from './getStepperCalculations';
@@ -0,0 +1,2 @@
1
+ export * from './getStepperCalculations';
2
+ export * from './amountDisplayHelpers';
@@ -1,4 +1,4 @@
1
- import { createContext, FC, ReactNode, use, useMemo } from 'react';
1
+ import { createContext, FC, ReactNode, useContext, useMemo } from 'react';
2
2
 
3
3
  export function createSafeContext<ContextValue extends object>(
4
4
  rootComponentName: string,
@@ -16,7 +16,7 @@ export function createSafeContext<ContextValue extends object>(
16
16
  Object.values(context ?? {}),
17
17
  ) as ContextValue;
18
18
 
19
- return <Context value={memoValue}>{children}</Context>;
19
+ return <Context.Provider value={memoValue}>{children}</Context.Provider>;
20
20
  };
21
21
 
22
22
  Provider.displayName = rootComponentName + 'Provider';
@@ -28,7 +28,7 @@ export function createSafeContext<ContextValue extends object>(
28
28
  consumerName: string;
29
29
  contextRequired: ContextRequired;
30
30
  }): ContextRequired extends true ? ContextValue : Partial<ContextValue> {
31
- const context = use(Context);
31
+ const context = useContext(Context);
32
32
 
33
33
  if (context) {
34
34
  return context;
@@ -0,0 +1,139 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { getStepperCalculations } from './getStepperCalculations';
3
+
4
+ describe('getStepperCalculations', () => {
5
+ const defaultInput = {
6
+ currentStep: 2,
7
+ totalSteps: 4,
8
+ size: 48,
9
+ };
10
+
11
+ it('should compute display label from currentStep and totalSteps', () => {
12
+ const result = getStepperCalculations(defaultInput);
13
+ expect(result.displayLabel).toBe('2/4');
14
+ });
15
+
16
+ it('should use custom label when provided', () => {
17
+ const result = getStepperCalculations({
18
+ ...defaultInput,
19
+ label: 'Step 2 of 4',
20
+ });
21
+ expect(result.displayLabel).toBe('Step 2 of 4');
22
+ });
23
+
24
+ it('should clamp display label to totalSteps', () => {
25
+ const result = getStepperCalculations({
26
+ ...defaultInput,
27
+ currentStep: 10,
28
+ });
29
+ expect(result.displayLabel).toBe('4/4');
30
+ });
31
+
32
+ it('should compute progress as currentStep / totalSteps', () => {
33
+ const result = getStepperCalculations(defaultInput);
34
+ expect(result.progress).toBe(0.5);
35
+ });
36
+
37
+ it('should clamp progress between 0 and 1', () => {
38
+ const negativeResult = getStepperCalculations({
39
+ ...defaultInput,
40
+ currentStep: -1,
41
+ });
42
+ expect(negativeResult.progress).toBe(0);
43
+
44
+ const overflowResult = getStepperCalculations({
45
+ ...defaultInput,
46
+ currentStep: 10,
47
+ });
48
+ expect(overflowResult.progress).toBe(1);
49
+ });
50
+
51
+ it('should handle zero totalSteps gracefully', () => {
52
+ const result = getStepperCalculations({
53
+ ...defaultInput,
54
+ totalSteps: 0,
55
+ });
56
+ expect(result.progress).toBe(0);
57
+ });
58
+
59
+ it('should compute correct SVG geometry for default size', () => {
60
+ const result = getStepperCalculations(defaultInput);
61
+ expect(result.cx).toBe(24);
62
+ expect(result.cy).toBe(24);
63
+ expect(result.r).toBe(22); // (48 - 4) / 2
64
+ });
65
+
66
+ it('should respect custom strokeWidth', () => {
67
+ const result = getStepperCalculations({
68
+ ...defaultInput,
69
+ strokeWidth: 6,
70
+ });
71
+ expect(result.r).toBe(21); // (48 - 6) / 2
72
+ });
73
+
74
+ it('should compute trackArcLength as 75% of circumference by default', () => {
75
+ const result = getStepperCalculations(defaultInput);
76
+ const expectedCircumference = 2 * Math.PI * 22;
77
+ expect(result.circumference).toBeCloseTo(expectedCircumference);
78
+ expect(result.trackArcLength).toBeCloseTo(expectedCircumference * 0.75);
79
+ });
80
+
81
+ it('should respect custom arcPercentage', () => {
82
+ const result = getStepperCalculations({
83
+ ...defaultInput,
84
+ arcPercentage: 0.5,
85
+ });
86
+ const expectedCircumference = 2 * Math.PI * 22;
87
+ expect(result.trackArcLength).toBeCloseTo(expectedCircumference * 0.5);
88
+ });
89
+
90
+ it('should show minimal dot when currentStep is 0', () => {
91
+ const result = getStepperCalculations({
92
+ ...defaultInput,
93
+ currentStep: 0,
94
+ });
95
+ expect(result.showMinimalDot).toBe(true);
96
+ expect(result.progressDashOffset).toBeCloseTo(result.trackArcLength - 2);
97
+ });
98
+
99
+ it('should show minimal dot when currentStep is negative', () => {
100
+ const result = getStepperCalculations({
101
+ ...defaultInput,
102
+ currentStep: -1,
103
+ });
104
+ expect(result.showMinimalDot).toBe(true);
105
+ expect(result.progressDashOffset).toBeCloseTo(result.trackArcLength - 2);
106
+ });
107
+
108
+ it('should show normal progress arc when currentStep > 0', () => {
109
+ const result = getStepperCalculations(defaultInput);
110
+ expect(result.showMinimalDot).toBe(false);
111
+ expect(result.progressDashArray).toContain(String(result.trackArcLength));
112
+ });
113
+
114
+ it('should compute progressDashOffset as 0 when progress is 100%', () => {
115
+ const result = getStepperCalculations({
116
+ ...defaultInput,
117
+ currentStep: 4,
118
+ totalSteps: 4,
119
+ });
120
+ expect(result.progressDashOffset).toBeCloseTo(0);
121
+ });
122
+
123
+ it('should compute progressDashOffset equal to trackArcLength when progress is 0%', () => {
124
+ const result = getStepperCalculations({
125
+ ...defaultInput,
126
+ currentStep: 0,
127
+ totalSteps: 4,
128
+ });
129
+ // When showMinimalDot, offset = trackArcLength - 2
130
+ expect(result.progressDashOffset).toBeCloseTo(result.trackArcLength - 2);
131
+ });
132
+
133
+ it('should compute trackDashArray consistently', () => {
134
+ const result = getStepperCalculations(defaultInput);
135
+ expect(result.trackDashArray).toBe(
136
+ `${result.trackArcLength} ${result.circumference}`,
137
+ );
138
+ });
139
+ });
@@ -0,0 +1,103 @@
1
+ export type StepperCalculationsInput = {
2
+ /** Current step number (1-based). Use 0 or negative for minimal dot. */
3
+ currentStep: number;
4
+ /** Total number of steps. */
5
+ totalSteps: number;
6
+ /** Size of the circular stepper in pixels. */
7
+ size: number;
8
+ /** Optional custom label. Defaults to "{currentStep}/{totalSteps}". */
9
+ label?: string;
10
+ /** Stroke width in pixels. @default 4 */
11
+ strokeWidth?: number;
12
+ /** Percentage of full circle to show as arc (0–1). @default 0.75 (270°) */
13
+ arcPercentage?: number;
14
+ };
15
+
16
+ export type StepperCalculationsOutput = {
17
+ /** Label text displayed in the center. */
18
+ displayLabel: string;
19
+ /** Progress value between 0 and 1. */
20
+ progress: number;
21
+ /** Circle radius in pixels. */
22
+ r: number;
23
+ /** Center x coordinate. */
24
+ cx: number;
25
+ /** Center y coordinate. */
26
+ cy: number;
27
+ /** Full circle circumference. */
28
+ circumference: number;
29
+ /** Length of the visible arc (track). */
30
+ trackArcLength: number;
31
+ /** strokeDasharray for the gray track circle. */
32
+ trackDashArray: string;
33
+ /** strokeDasharray for the progress (purple) circle. */
34
+ progressDashArray: string;
35
+ /** strokeDashoffset for the progress (purple) circle. */
36
+ progressDashOffset: number;
37
+ /** Whether to show minimal dot instead of progress arc. */
38
+ showMinimalDot: boolean;
39
+ };
40
+
41
+ /**
42
+ * Computes all SVG-related values needed to render a circular stepper.
43
+ *
44
+ * This is a pure utility shared between React (web) and React Native
45
+ * implementations of the Stepper component.
46
+ *
47
+ * @example
48
+ * ```ts
49
+ * const calcs = getStepperCalculations({
50
+ * currentStep: 2,
51
+ * totalSteps: 4,
52
+ * size: 48,
53
+ * });
54
+ *
55
+ * // calcs.displayLabel → '2/4'
56
+ * // calcs.progress → 0.5
57
+ * // calcs.trackDashArray → '<trackArcLength> <circumference>'
58
+ * // calcs.progressDashArray → '<trackArcLength> <circumference>'
59
+ * // calcs.progressDashOffset → offset for 50% fill
60
+ * ```
61
+ */
62
+ export const getStepperCalculations = ({
63
+ currentStep,
64
+ totalSteps,
65
+ size,
66
+ label,
67
+ strokeWidth = 4,
68
+ arcPercentage = 0.75,
69
+ }: StepperCalculationsInput): StepperCalculationsOutput => {
70
+ const displayLabel =
71
+ label ?? `${Math.min(currentStep, totalSteps)}/${totalSteps}`;
72
+ const progress =
73
+ totalSteps <= 0 ? 0 : Math.min(1, Math.max(0, currentStep / totalSteps));
74
+
75
+ // SVG circle geometry
76
+ const r = (size - strokeWidth) / 2;
77
+ const cx = size / 2;
78
+ const cy = size / 2;
79
+ const circumference = 2 * Math.PI * r;
80
+
81
+ // Arc calculations
82
+ const trackArcLength = circumference * arcPercentage;
83
+ const dashOffset = trackArcLength * (1 - progress);
84
+
85
+ // Minimal dot handling (currentStep <= 0 means "not started")
86
+ const showMinimalDot = currentStep <= 0;
87
+ const progressDashArray = `${trackArcLength} ${circumference}`;
88
+ const progressDashOffset = showMinimalDot ? trackArcLength - 2 : dashOffset;
89
+
90
+ return {
91
+ displayLabel,
92
+ progress,
93
+ r,
94
+ cx,
95
+ cy,
96
+ circumference,
97
+ trackArcLength,
98
+ trackDashArray: `${trackArcLength} ${circumference}`,
99
+ progressDashArray,
100
+ progressDashOffset,
101
+ showMinimalDot,
102
+ };
103
+ };
@@ -0,0 +1,5 @@
1
+ export {
2
+ getStepperCalculations,
3
+ type StepperCalculationsInput,
4
+ type StepperCalculationsOutput,
5
+ } from './getStepperCalculations';