@bacon-pos/ui 0.1.0 → 0.2.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.
- package/README.md +4 -3
- package/lib/commonjs/components/CodeField/CodeField.js +127 -0
- package/lib/commonjs/components/CodeField/CodeField.js.map +1 -0
- package/lib/commonjs/components/CodeField/CodeField.stories.js +76 -0
- package/lib/commonjs/components/CodeField/CodeField.stories.js.map +1 -0
- package/lib/commonjs/components/CodeField/CodeField.test.js +79 -0
- package/lib/commonjs/components/CodeField/CodeField.test.js.map +1 -0
- package/lib/commonjs/components/CodeField/index.js +13 -0
- package/lib/commonjs/components/CodeField/index.js.map +1 -0
- package/lib/commonjs/components/Spinner/Spinner.js +114 -0
- package/lib/commonjs/components/Spinner/Spinner.js.map +1 -0
- package/lib/commonjs/components/Spinner/Spinner.stories.js +72 -0
- package/lib/commonjs/components/Spinner/Spinner.stories.js.map +1 -0
- package/lib/commonjs/components/Spinner/Spinner.test.js +34 -0
- package/lib/commonjs/components/Spinner/Spinner.test.js.map +1 -0
- package/lib/commonjs/components/Spinner/index.js +13 -0
- package/lib/commonjs/components/Spinner/index.js.map +1 -0
- package/lib/commonjs/components/index.js +22 -11
- package/lib/commonjs/components/index.js.map +1 -1
- package/lib/module/components/CodeField/CodeField.js +125 -0
- package/lib/module/components/CodeField/CodeField.js.map +1 -0
- package/lib/module/components/CodeField/CodeField.stories.js +72 -0
- package/lib/module/components/CodeField/CodeField.stories.js.map +1 -0
- package/lib/module/components/CodeField/CodeField.test.js +78 -0
- package/lib/module/components/CodeField/CodeField.test.js.map +1 -0
- package/lib/module/components/CodeField/index.js +4 -0
- package/lib/module/components/CodeField/index.js.map +1 -0
- package/lib/module/components/Spinner/Spinner.js +109 -0
- package/lib/module/components/Spinner/Spinner.js.map +1 -0
- package/lib/module/components/Spinner/Spinner.stories.js +68 -0
- package/lib/module/components/Spinner/Spinner.stories.js.map +1 -0
- package/lib/module/components/Spinner/Spinner.test.js +33 -0
- package/lib/module/components/Spinner/Spinner.test.js.map +1 -0
- package/lib/module/components/Spinner/index.js +4 -0
- package/lib/module/components/Spinner/index.js.map +1 -0
- package/lib/module/components/index.js +1 -0
- package/lib/module/components/index.js.map +1 -1
- package/lib/typescript/commonjs/components/CodeField/CodeField.d.ts +40 -0
- package/lib/typescript/commonjs/components/CodeField/CodeField.d.ts.map +1 -0
- package/lib/typescript/commonjs/components/CodeField/index.d.ts +2 -0
- package/lib/typescript/commonjs/components/CodeField/index.d.ts.map +1 -0
- package/lib/typescript/commonjs/components/Spinner/Spinner.d.ts +31 -0
- package/lib/typescript/commonjs/components/Spinner/Spinner.d.ts.map +1 -0
- package/lib/typescript/commonjs/components/Spinner/index.d.ts +2 -0
- package/lib/typescript/commonjs/components/Spinner/index.d.ts.map +1 -0
- package/lib/typescript/commonjs/components/index.d.ts +1 -0
- package/lib/typescript/commonjs/components/index.d.ts.map +1 -1
- package/lib/typescript/module/components/CodeField/CodeField.d.ts +40 -0
- package/lib/typescript/module/components/CodeField/CodeField.d.ts.map +1 -0
- package/lib/typescript/module/components/CodeField/index.d.ts +2 -0
- package/lib/typescript/module/components/CodeField/index.d.ts.map +1 -0
- package/lib/typescript/module/components/Spinner/Spinner.d.ts +31 -0
- package/lib/typescript/module/components/Spinner/Spinner.d.ts.map +1 -0
- package/lib/typescript/module/components/Spinner/index.d.ts +2 -0
- package/lib/typescript/module/components/Spinner/index.d.ts.map +1 -0
- package/lib/typescript/module/components/index.d.ts +1 -0
- package/lib/typescript/module/components/index.d.ts.map +1 -1
- package/package.json +14 -16
- package/src/components/CodeField/CodeField.tsx +160 -0
- package/src/components/CodeField/index.ts +1 -0
- package/src/components/Spinner/Spinner.tsx +128 -0
- package/src/components/Spinner/index.ts +1 -0
- package/src/components/index.ts +1 -0
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
import React from 'react';
|
|
4
|
+
import { render, screen } from '@testing-library/react-native';
|
|
5
|
+
import { Spinner } from "./Spinner.js";
|
|
6
|
+
import { ThemeProvider } from "../../theme/index.js";
|
|
7
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
8
|
+
function renderWithTheme(ui) {
|
|
9
|
+
return render(/*#__PURE__*/_jsx(ThemeProvider, {
|
|
10
|
+
children: ui
|
|
11
|
+
}));
|
|
12
|
+
}
|
|
13
|
+
describe('Spinner', () => {
|
|
14
|
+
it('renders with a progressbar role', () => {
|
|
15
|
+
renderWithTheme(/*#__PURE__*/_jsx(Spinner, {
|
|
16
|
+
accessibilityLabel: "Loading"
|
|
17
|
+
}));
|
|
18
|
+
expect(screen.getByLabelText('Loading')).toBeTruthy();
|
|
19
|
+
});
|
|
20
|
+
it('renders its label', () => {
|
|
21
|
+
renderWithTheme(/*#__PURE__*/_jsx(Spinner, {
|
|
22
|
+
label: "Wait a moment please..."
|
|
23
|
+
}));
|
|
24
|
+
expect(screen.getByText('Wait a moment please...')).toBeTruthy();
|
|
25
|
+
});
|
|
26
|
+
it('renders without a label when none is given', () => {
|
|
27
|
+
renderWithTheme(/*#__PURE__*/_jsx(Spinner, {
|
|
28
|
+
accessibilityLabel: "Loading"
|
|
29
|
+
}));
|
|
30
|
+
expect(screen.queryByText('Loading')).toBeNull();
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
//# sourceMappingURL=Spinner.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"names":["React","render","screen","Spinner","ThemeProvider","jsx","_jsx","renderWithTheme","ui","children","describe","it","accessibilityLabel","expect","getByLabelText","toBeTruthy","label","getByText","queryByText","toBeNull"],"sourceRoot":"..\\..\\..\\..\\src","sources":["components/Spinner/Spinner.test.tsx"],"mappings":";;AAAA,OAAOA,KAAK,MAAM,OAAO;AACzB,SAASC,MAAM,EAAEC,MAAM,QAAQ,+BAA+B;AAC9D,SAASC,OAAO,QAAQ,cAAW;AACnC,SAASC,aAAa,QAAQ,sBAAa;AAAC,SAAAC,GAAA,IAAAC,IAAA;AAE5C,SAASC,eAAeA,CAACC,EAAsB,EAAE;EAC/C,OAAOP,MAAM,cAACK,IAAA,CAACF,aAAa;IAAAK,QAAA,EAAED;EAAE,CAAgB,CAAC,CAAC;AACpD;AAEAE,QAAQ,CAAC,SAAS,EAAE,MAAM;EACxBC,EAAE,CAAC,iCAAiC,EAAE,MAAM;IAC1CJ,eAAe,cAACD,IAAA,CAACH,OAAO;MAACS,kBAAkB,EAAC;IAAS,CAAE,CAAC,CAAC;IACzDC,MAAM,CAACX,MAAM,CAACY,cAAc,CAAC,SAAS,CAAC,CAAC,CAACC,UAAU,CAAC,CAAC;EACvD,CAAC,CAAC;EAEFJ,EAAE,CAAC,mBAAmB,EAAE,MAAM;IAC5BJ,eAAe,cAACD,IAAA,CAACH,OAAO;MAACa,KAAK,EAAC;IAAyB,CAAE,CAAC,CAAC;IAC5DH,MAAM,CAACX,MAAM,CAACe,SAAS,CAAC,yBAAyB,CAAC,CAAC,CAACF,UAAU,CAAC,CAAC;EAClE,CAAC,CAAC;EAEFJ,EAAE,CAAC,4CAA4C,EAAE,MAAM;IACrDJ,eAAe,cAACD,IAAA,CAACH,OAAO;MAACS,kBAAkB,EAAC;IAAS,CAAE,CAAC,CAAC;IACzDC,MAAM,CAACX,MAAM,CAACgB,WAAW,CAAC,SAAS,CAAC,CAAC,CAACC,QAAQ,CAAC,CAAC;EAClD,CAAC,CAAC;AACJ,CAAC,CAAC","ignoreList":[]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"names":["Spinner"],"sourceRoot":"..\\..\\..\\..\\src","sources":["components/Spinner/index.ts"],"mappings":";;AAAA,SAASA,OAAO,QAA6C,cAAW","ignoreList":[]}
|
|
@@ -6,6 +6,7 @@ export * from "./Button/index.js";
|
|
|
6
6
|
export * from "./Card/index.js";
|
|
7
7
|
export * from "./Checkbox/index.js";
|
|
8
8
|
export * from "./Chip/index.js";
|
|
9
|
+
export * from "./CodeField/index.js";
|
|
9
10
|
export * from "./CodeInput/index.js";
|
|
10
11
|
export * from "./Icon/index.js";
|
|
11
12
|
export * from "./IconButton/index.js";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"names":[],"sourceRoot":"..\\..\\..\\src","sources":["components/index.ts"],"mappings":";;AAAA,cAAc,mBAAU;AACxB,cAAc,kBAAS;AACvB,cAAc,mBAAU;AACxB,cAAc,iBAAQ;AACtB,cAAc,qBAAY;AAC1B,cAAc,iBAAQ;AACtB,cAAc,sBAAa;AAC3B,cAAc,iBAAQ;AACtB,cAAc,uBAAc;AAC5B,cAAc,kBAAS;AACvB,cAAc,uBAAc;AAC5B,cAAc,wBAAe;AAC7B,cAAc,4BAAmB;AACjC,cAAc,kBAAS;AACvB,cAAc,oBAAW;AACzB,cAAc,mBAAU;AACxB,cAAc,iBAAQ","ignoreList":[]}
|
|
1
|
+
{"version":3,"names":[],"sourceRoot":"..\\..\\..\\src","sources":["components/index.ts"],"mappings":";;AAAA,cAAc,mBAAU;AACxB,cAAc,kBAAS;AACvB,cAAc,mBAAU;AACxB,cAAc,iBAAQ;AACtB,cAAc,qBAAY;AAC1B,cAAc,iBAAQ;AACtB,cAAc,sBAAa;AAC3B,cAAc,sBAAa;AAC3B,cAAc,iBAAQ;AACtB,cAAc,uBAAc;AAC5B,cAAc,kBAAS;AACvB,cAAc,uBAAc;AAC5B,cAAc,wBAAe;AAC7B,cAAc,4BAAmB;AACjC,cAAc,kBAAS;AACvB,cAAc,oBAAW;AACzB,cAAc,mBAAU;AACxB,cAAc,iBAAQ","ignoreList":[]}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { type StyleProp, type ViewStyle } from 'react-native';
|
|
2
|
+
export type CodeFieldMode = 'numeric' | 'alpha' | 'alphanumeric';
|
|
3
|
+
export interface CodeFieldProps {
|
|
4
|
+
/** Current code. */
|
|
5
|
+
value: string;
|
|
6
|
+
/** Called with the sanitized, upper-cased code as it changes. */
|
|
7
|
+
onChangeText: (value: string) => void;
|
|
8
|
+
/** Number of cells. Defaults to 6. */
|
|
9
|
+
length?: number;
|
|
10
|
+
/** Allowed characters. Defaults to `numeric`. */
|
|
11
|
+
mode?: CodeFieldMode;
|
|
12
|
+
/** Called once every cell is filled. */
|
|
13
|
+
onComplete?: (value: string) => void;
|
|
14
|
+
/** Disable input and dim the cells. */
|
|
15
|
+
disabled?: boolean;
|
|
16
|
+
/** Focus on mount. */
|
|
17
|
+
autoFocus?: boolean;
|
|
18
|
+
/** Cell width in px (Figma default 126). */
|
|
19
|
+
cellWidth?: number;
|
|
20
|
+
/** Cell height in px (Figma default 58). */
|
|
21
|
+
cellHeight?: number;
|
|
22
|
+
/** Gap between cells in px (Figma default 24). */
|
|
23
|
+
gap?: number;
|
|
24
|
+
/** Font family for the digits. Defaults to the theme's regular Inter. */
|
|
25
|
+
fontFamily?: string;
|
|
26
|
+
/** Font size for the digits (Figma default 18). */
|
|
27
|
+
fontSize?: number;
|
|
28
|
+
/** Accessibility label for the hidden field. Defaults to "Code". */
|
|
29
|
+
accessibilityLabel?: string;
|
|
30
|
+
/** Override the container style. */
|
|
31
|
+
style?: StyleProp<ViewStyle>;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Code field — a row of boxes for entering a code (numeric / alpha /
|
|
35
|
+
* alphanumeric, always upper-cased). Backed by a single hidden input, so paste,
|
|
36
|
+
* backspace-to-previous and type-to-next all work natively. The active cell
|
|
37
|
+
* shows the lime focus ring. Cell size, gap, font and font size are configurable.
|
|
38
|
+
*/
|
|
39
|
+
export declare function CodeField({ value, onChangeText, length, mode, onComplete, disabled, autoFocus, cellWidth, cellHeight, gap, fontFamily, fontSize, accessibilityLabel, style, }: CodeFieldProps): import("react/jsx-runtime").JSX.Element;
|
|
40
|
+
//# sourceMappingURL=CodeField.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CodeField.d.ts","sourceRoot":"","sources":["../../../../../src/components/CodeField/CodeField.tsx"],"names":[],"mappings":"AACA,OAAO,EAIL,KAAK,SAAS,EAEd,KAAK,SAAS,EACf,MAAM,cAAc,CAAC;AAOtB,MAAM,MAAM,aAAa,GAAG,SAAS,GAAG,OAAO,GAAG,cAAc,CAAC;AASjE,MAAM,WAAW,cAAc;IAC7B,oBAAoB;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,iEAAiE;IACjE,YAAY,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACtC,sCAAsC;IACtC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,iDAAiD;IACjD,IAAI,CAAC,EAAE,aAAa,CAAC;IACrB,wCAAwC;IACxC,UAAU,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACrC,uCAAuC;IACvC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,sBAAsB;IACtB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,4CAA4C;IAC5C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,4CAA4C;IAC5C,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,kDAAkD;IAClD,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,yEAAyE;IACzE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,mDAAmD;IACnD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,oEAAoE;IACpE,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,oCAAoC;IACpC,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;CAC9B;AAED;;;;;GAKG;AACH,wBAAgB,SAAS,CAAC,EACxB,KAAK,EACL,YAAY,EACZ,MAAU,EACV,IAAgB,EAChB,UAAU,EACV,QAAgB,EAChB,SAAiB,EACjB,SAAe,EACf,UAAe,EACf,GAAQ,EACR,UAAU,EACV,QAAa,EACb,kBAA2B,EAC3B,KAAK,GACN,EAAE,cAAc,2CAmFhB"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/components/CodeField/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,KAAK,cAAc,EAAE,KAAK,aAAa,EAAE,MAAM,aAAa,CAAC"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { type StyleProp, type ViewStyle } from 'react-native';
|
|
2
|
+
export type SpinnerSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl';
|
|
3
|
+
export interface SpinnerProps {
|
|
4
|
+
/** Size preset: xs / sm / md / lg / xl. Defaults to `md`. */
|
|
5
|
+
size?: SpinnerSize;
|
|
6
|
+
/** Ring color. Defaults to the brand lime action color. */
|
|
7
|
+
color?: string;
|
|
8
|
+
/** Ring thickness in px. Defaults to ~7% of the diameter. */
|
|
9
|
+
thickness?: number;
|
|
10
|
+
/** Optional label shown next to the spinner. */
|
|
11
|
+
label?: string;
|
|
12
|
+
/** Label position relative to the spinner. Defaults to `bottom`. */
|
|
13
|
+
labelPosition?: 'top' | 'bottom';
|
|
14
|
+
/** Distance between spinner and label. Defaults to 8. */
|
|
15
|
+
gap?: number;
|
|
16
|
+
/** Label font size. Defaults to 16. */
|
|
17
|
+
labelSize?: number;
|
|
18
|
+
/** Label color (token or raw). Defaults to secondary text. */
|
|
19
|
+
labelColor?: string;
|
|
20
|
+
/** Rotation duration in ms (one full turn). Defaults to 900. */
|
|
21
|
+
duration?: number;
|
|
22
|
+
style?: StyleProp<ViewStyle>;
|
|
23
|
+
accessibilityLabel?: string;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Spinner — an infinitely rotating ring (react-native-svg + Animated, native
|
|
27
|
+
* driver for smooth motion). Configurable size, color, thickness and an
|
|
28
|
+
* optional label (top/bottom, with adjustable gap and font size).
|
|
29
|
+
*/
|
|
30
|
+
export declare function Spinner({ size, color, thickness, label, labelPosition, gap, labelSize, labelColor, duration, style, accessibilityLabel, }: SpinnerProps): import("react/jsx-runtime").JSX.Element;
|
|
31
|
+
//# sourceMappingURL=Spinner.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Spinner.d.ts","sourceRoot":"","sources":["../../../../../src/components/Spinner/Spinner.tsx"],"names":[],"mappings":"AACA,OAAO,EAAoC,KAAK,SAAS,EAAkB,KAAK,SAAS,EAAE,MAAM,cAAc,CAAC;AAKhH,MAAM,MAAM,WAAW,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC;AAK3D,MAAM,WAAW,YAAY;IAC3B,6DAA6D;IAC7D,IAAI,CAAC,EAAE,WAAW,CAAC;IACnB,2DAA2D;IAC3D,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,6DAA6D;IAC7D,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,gDAAgD;IAChD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,oEAAoE;IACpE,aAAa,CAAC,EAAE,KAAK,GAAG,QAAQ,CAAC;IACjC,yDAAyD;IACzD,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,uCAAuC;IACvC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,8DAA8D;IAC9D,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,gEAAgE;IAChE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC7B,kBAAkB,CAAC,EAAE,MAAM,CAAC;CAC7B;AAED;;;;GAIG;AACH,wBAAgB,OAAO,CAAC,EACtB,IAAW,EACX,KAAK,EACL,SAAS,EACT,KAAK,EACL,aAAwB,EACxB,GAAO,EACP,SAAc,EACd,UAAU,EACV,QAAc,EACd,KAAK,EACL,kBAAkB,GACnB,EAAE,YAAY,2CA4Ed"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/components/Spinner/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,KAAK,YAAY,EAAE,KAAK,WAAW,EAAE,MAAM,WAAW,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/components/index.ts"],"names":[],"mappings":"AAAA,cAAc,UAAU,CAAC;AACzB,cAAc,SAAS,CAAC;AACxB,cAAc,UAAU,CAAC;AACzB,cAAc,QAAQ,CAAC;AACvB,cAAc,YAAY,CAAC;AAC3B,cAAc,QAAQ,CAAC;AACvB,cAAc,aAAa,CAAC;AAC5B,cAAc,QAAQ,CAAC;AACvB,cAAc,cAAc,CAAC;AAC7B,cAAc,SAAS,CAAC;AACxB,cAAc,cAAc,CAAC;AAC7B,cAAc,eAAe,CAAC;AAC9B,cAAc,mBAAmB,CAAC;AAClC,cAAc,SAAS,CAAC;AACxB,cAAc,WAAW,CAAC;AAC1B,cAAc,UAAU,CAAC;AACzB,cAAc,QAAQ,CAAC;AAEvB,YAAY,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/components/index.ts"],"names":[],"mappings":"AAAA,cAAc,UAAU,CAAC;AACzB,cAAc,SAAS,CAAC;AACxB,cAAc,UAAU,CAAC;AACzB,cAAc,QAAQ,CAAC;AACvB,cAAc,YAAY,CAAC;AAC3B,cAAc,QAAQ,CAAC;AACvB,cAAc,aAAa,CAAC;AAC5B,cAAc,aAAa,CAAC;AAC5B,cAAc,QAAQ,CAAC;AACvB,cAAc,cAAc,CAAC;AAC7B,cAAc,SAAS,CAAC;AACxB,cAAc,cAAc,CAAC;AAC7B,cAAc,eAAe,CAAC;AAC9B,cAAc,mBAAmB,CAAC;AAClC,cAAc,SAAS,CAAC;AACxB,cAAc,WAAW,CAAC;AAC1B,cAAc,UAAU,CAAC;AACzB,cAAc,QAAQ,CAAC;AAEvB,YAAY,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { type StyleProp, type ViewStyle } from 'react-native';
|
|
2
|
+
export type CodeFieldMode = 'numeric' | 'alpha' | 'alphanumeric';
|
|
3
|
+
export interface CodeFieldProps {
|
|
4
|
+
/** Current code. */
|
|
5
|
+
value: string;
|
|
6
|
+
/** Called with the sanitized, upper-cased code as it changes. */
|
|
7
|
+
onChangeText: (value: string) => void;
|
|
8
|
+
/** Number of cells. Defaults to 6. */
|
|
9
|
+
length?: number;
|
|
10
|
+
/** Allowed characters. Defaults to `numeric`. */
|
|
11
|
+
mode?: CodeFieldMode;
|
|
12
|
+
/** Called once every cell is filled. */
|
|
13
|
+
onComplete?: (value: string) => void;
|
|
14
|
+
/** Disable input and dim the cells. */
|
|
15
|
+
disabled?: boolean;
|
|
16
|
+
/** Focus on mount. */
|
|
17
|
+
autoFocus?: boolean;
|
|
18
|
+
/** Cell width in px (Figma default 126). */
|
|
19
|
+
cellWidth?: number;
|
|
20
|
+
/** Cell height in px (Figma default 58). */
|
|
21
|
+
cellHeight?: number;
|
|
22
|
+
/** Gap between cells in px (Figma default 24). */
|
|
23
|
+
gap?: number;
|
|
24
|
+
/** Font family for the digits. Defaults to the theme's regular Inter. */
|
|
25
|
+
fontFamily?: string;
|
|
26
|
+
/** Font size for the digits (Figma default 18). */
|
|
27
|
+
fontSize?: number;
|
|
28
|
+
/** Accessibility label for the hidden field. Defaults to "Code". */
|
|
29
|
+
accessibilityLabel?: string;
|
|
30
|
+
/** Override the container style. */
|
|
31
|
+
style?: StyleProp<ViewStyle>;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Code field — a row of boxes for entering a code (numeric / alpha /
|
|
35
|
+
* alphanumeric, always upper-cased). Backed by a single hidden input, so paste,
|
|
36
|
+
* backspace-to-previous and type-to-next all work natively. The active cell
|
|
37
|
+
* shows the lime focus ring. Cell size, gap, font and font size are configurable.
|
|
38
|
+
*/
|
|
39
|
+
export declare function CodeField({ value, onChangeText, length, mode, onComplete, disabled, autoFocus, cellWidth, cellHeight, gap, fontFamily, fontSize, accessibilityLabel, style, }: CodeFieldProps): import("react/jsx-runtime").JSX.Element;
|
|
40
|
+
//# sourceMappingURL=CodeField.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CodeField.d.ts","sourceRoot":"","sources":["../../../../../src/components/CodeField/CodeField.tsx"],"names":[],"mappings":"AACA,OAAO,EAIL,KAAK,SAAS,EAEd,KAAK,SAAS,EACf,MAAM,cAAc,CAAC;AAOtB,MAAM,MAAM,aAAa,GAAG,SAAS,GAAG,OAAO,GAAG,cAAc,CAAC;AASjE,MAAM,WAAW,cAAc;IAC7B,oBAAoB;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,iEAAiE;IACjE,YAAY,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACtC,sCAAsC;IACtC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,iDAAiD;IACjD,IAAI,CAAC,EAAE,aAAa,CAAC;IACrB,wCAAwC;IACxC,UAAU,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACrC,uCAAuC;IACvC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,sBAAsB;IACtB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,4CAA4C;IAC5C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,4CAA4C;IAC5C,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,kDAAkD;IAClD,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,yEAAyE;IACzE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,mDAAmD;IACnD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,oEAAoE;IACpE,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,oCAAoC;IACpC,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;CAC9B;AAED;;;;;GAKG;AACH,wBAAgB,SAAS,CAAC,EACxB,KAAK,EACL,YAAY,EACZ,MAAU,EACV,IAAgB,EAChB,UAAU,EACV,QAAgB,EAChB,SAAiB,EACjB,SAAe,EACf,UAAe,EACf,GAAQ,EACR,UAAU,EACV,QAAa,EACb,kBAA2B,EAC3B,KAAK,GACN,EAAE,cAAc,2CAmFhB"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/components/CodeField/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,KAAK,cAAc,EAAE,KAAK,aAAa,EAAE,MAAM,aAAa,CAAC"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { type StyleProp, type ViewStyle } from 'react-native';
|
|
2
|
+
export type SpinnerSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl';
|
|
3
|
+
export interface SpinnerProps {
|
|
4
|
+
/** Size preset: xs / sm / md / lg / xl. Defaults to `md`. */
|
|
5
|
+
size?: SpinnerSize;
|
|
6
|
+
/** Ring color. Defaults to the brand lime action color. */
|
|
7
|
+
color?: string;
|
|
8
|
+
/** Ring thickness in px. Defaults to ~7% of the diameter. */
|
|
9
|
+
thickness?: number;
|
|
10
|
+
/** Optional label shown next to the spinner. */
|
|
11
|
+
label?: string;
|
|
12
|
+
/** Label position relative to the spinner. Defaults to `bottom`. */
|
|
13
|
+
labelPosition?: 'top' | 'bottom';
|
|
14
|
+
/** Distance between spinner and label. Defaults to 8. */
|
|
15
|
+
gap?: number;
|
|
16
|
+
/** Label font size. Defaults to 16. */
|
|
17
|
+
labelSize?: number;
|
|
18
|
+
/** Label color (token or raw). Defaults to secondary text. */
|
|
19
|
+
labelColor?: string;
|
|
20
|
+
/** Rotation duration in ms (one full turn). Defaults to 900. */
|
|
21
|
+
duration?: number;
|
|
22
|
+
style?: StyleProp<ViewStyle>;
|
|
23
|
+
accessibilityLabel?: string;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Spinner — an infinitely rotating ring (react-native-svg + Animated, native
|
|
27
|
+
* driver for smooth motion). Configurable size, color, thickness and an
|
|
28
|
+
* optional label (top/bottom, with adjustable gap and font size).
|
|
29
|
+
*/
|
|
30
|
+
export declare function Spinner({ size, color, thickness, label, labelPosition, gap, labelSize, labelColor, duration, style, accessibilityLabel, }: SpinnerProps): import("react/jsx-runtime").JSX.Element;
|
|
31
|
+
//# sourceMappingURL=Spinner.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Spinner.d.ts","sourceRoot":"","sources":["../../../../../src/components/Spinner/Spinner.tsx"],"names":[],"mappings":"AACA,OAAO,EAAoC,KAAK,SAAS,EAAkB,KAAK,SAAS,EAAE,MAAM,cAAc,CAAC;AAKhH,MAAM,MAAM,WAAW,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC;AAK3D,MAAM,WAAW,YAAY;IAC3B,6DAA6D;IAC7D,IAAI,CAAC,EAAE,WAAW,CAAC;IACnB,2DAA2D;IAC3D,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,6DAA6D;IAC7D,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,gDAAgD;IAChD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,oEAAoE;IACpE,aAAa,CAAC,EAAE,KAAK,GAAG,QAAQ,CAAC;IACjC,yDAAyD;IACzD,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,uCAAuC;IACvC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,8DAA8D;IAC9D,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,gEAAgE;IAChE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC7B,kBAAkB,CAAC,EAAE,MAAM,CAAC;CAC7B;AAED;;;;GAIG;AACH,wBAAgB,OAAO,CAAC,EACtB,IAAW,EACX,KAAK,EACL,SAAS,EACT,KAAK,EACL,aAAwB,EACxB,GAAO,EACP,SAAc,EACd,UAAU,EACV,QAAc,EACd,KAAK,EACL,kBAAkB,GACnB,EAAE,YAAY,2CA4Ed"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/components/Spinner/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,KAAK,YAAY,EAAE,KAAK,WAAW,EAAE,MAAM,WAAW,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/components/index.ts"],"names":[],"mappings":"AAAA,cAAc,UAAU,CAAC;AACzB,cAAc,SAAS,CAAC;AACxB,cAAc,UAAU,CAAC;AACzB,cAAc,QAAQ,CAAC;AACvB,cAAc,YAAY,CAAC;AAC3B,cAAc,QAAQ,CAAC;AACvB,cAAc,aAAa,CAAC;AAC5B,cAAc,QAAQ,CAAC;AACvB,cAAc,cAAc,CAAC;AAC7B,cAAc,SAAS,CAAC;AACxB,cAAc,cAAc,CAAC;AAC7B,cAAc,eAAe,CAAC;AAC9B,cAAc,mBAAmB,CAAC;AAClC,cAAc,SAAS,CAAC;AACxB,cAAc,WAAW,CAAC;AAC1B,cAAc,UAAU,CAAC;AACzB,cAAc,QAAQ,CAAC;AAEvB,YAAY,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/components/index.ts"],"names":[],"mappings":"AAAA,cAAc,UAAU,CAAC;AACzB,cAAc,SAAS,CAAC;AACxB,cAAc,UAAU,CAAC;AACzB,cAAc,QAAQ,CAAC;AACvB,cAAc,YAAY,CAAC;AAC3B,cAAc,QAAQ,CAAC;AACvB,cAAc,aAAa,CAAC;AAC5B,cAAc,aAAa,CAAC;AAC5B,cAAc,QAAQ,CAAC;AACvB,cAAc,cAAc,CAAC;AAC7B,cAAc,SAAS,CAAC;AACxB,cAAc,cAAc,CAAC;AAC7B,cAAc,eAAe,CAAC;AAC9B,cAAc,mBAAmB,CAAC;AAClC,cAAc,SAAS,CAAC;AACxB,cAAc,WAAW,CAAC;AAC1B,cAAc,UAAU,CAAC;AACzB,cAAc,QAAQ,CAAC;AAEvB,YAAY,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bacon-pos/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Bacon POS design system — React Native component library",
|
|
5
5
|
"main": "./lib/commonjs/index.js",
|
|
6
6
|
"module": "./lib/module/index.js",
|
|
@@ -28,19 +28,6 @@
|
|
|
28
28
|
"!**/*.stories.tsx"
|
|
29
29
|
],
|
|
30
30
|
"sideEffects": false,
|
|
31
|
-
"scripts": {
|
|
32
|
-
"clean": "del-cli lib",
|
|
33
|
-
"build": "bob build",
|
|
34
|
-
"prepare": "bob build",
|
|
35
|
-
"typecheck": "tsc --noEmit",
|
|
36
|
-
"gen:icons": "node scripts/generate-icons.mjs",
|
|
37
|
-
"gen:graphics": "node scripts/generate-graphics.mjs",
|
|
38
|
-
"test": "jest",
|
|
39
|
-
"storybook": "storybook dev -p 6006",
|
|
40
|
-
"build-storybook": "storybook build",
|
|
41
|
-
"release": "changeset publish",
|
|
42
|
-
"version": "changeset version"
|
|
43
|
-
},
|
|
44
31
|
"keywords": [
|
|
45
32
|
"react-native",
|
|
46
33
|
"expo",
|
|
@@ -112,5 +99,16 @@
|
|
|
112
99
|
"typescript"
|
|
113
100
|
]
|
|
114
101
|
},
|
|
115
|
-
"
|
|
116
|
-
|
|
102
|
+
"scripts": {
|
|
103
|
+
"clean": "del-cli lib",
|
|
104
|
+
"build": "bob build",
|
|
105
|
+
"typecheck": "tsc --noEmit",
|
|
106
|
+
"gen:icons": "node scripts/generate-icons.mjs",
|
|
107
|
+
"gen:graphics": "node scripts/generate-graphics.mjs",
|
|
108
|
+
"test": "jest",
|
|
109
|
+
"storybook": "storybook dev -p 6006",
|
|
110
|
+
"build-storybook": "storybook build",
|
|
111
|
+
"release": "changeset publish",
|
|
112
|
+
"version": "changeset version"
|
|
113
|
+
}
|
|
114
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { useRef, useState } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
Pressable,
|
|
4
|
+
TextInput,
|
|
5
|
+
View,
|
|
6
|
+
type StyleProp,
|
|
7
|
+
type TextStyle,
|
|
8
|
+
type ViewStyle,
|
|
9
|
+
} from 'react-native';
|
|
10
|
+
import { useTheme } from '../../theme';
|
|
11
|
+
import { Text } from '../Text/Text';
|
|
12
|
+
|
|
13
|
+
/** Removes the browser's default focus outline on web (no-op on native). */
|
|
14
|
+
const WEB_NO_OUTLINE = { outlineStyle: 'none', outlineWidth: 0 } as unknown as TextStyle;
|
|
15
|
+
|
|
16
|
+
export type CodeFieldMode = 'numeric' | 'alpha' | 'alphanumeric';
|
|
17
|
+
|
|
18
|
+
/** Chars to strip per mode (applied after upper-casing). */
|
|
19
|
+
const DISALLOWED: Record<CodeFieldMode, RegExp> = {
|
|
20
|
+
numeric: /[^0-9]/g,
|
|
21
|
+
alpha: /[^A-Z]/g,
|
|
22
|
+
alphanumeric: /[^A-Z0-9]/g,
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export interface CodeFieldProps {
|
|
26
|
+
/** Current code. */
|
|
27
|
+
value: string;
|
|
28
|
+
/** Called with the sanitized, upper-cased code as it changes. */
|
|
29
|
+
onChangeText: (value: string) => void;
|
|
30
|
+
/** Number of cells. Defaults to 6. */
|
|
31
|
+
length?: number;
|
|
32
|
+
/** Allowed characters. Defaults to `numeric`. */
|
|
33
|
+
mode?: CodeFieldMode;
|
|
34
|
+
/** Called once every cell is filled. */
|
|
35
|
+
onComplete?: (value: string) => void;
|
|
36
|
+
/** Disable input and dim the cells. */
|
|
37
|
+
disabled?: boolean;
|
|
38
|
+
/** Focus on mount. */
|
|
39
|
+
autoFocus?: boolean;
|
|
40
|
+
/** Cell width in px (Figma default 126). */
|
|
41
|
+
cellWidth?: number;
|
|
42
|
+
/** Cell height in px (Figma default 58). */
|
|
43
|
+
cellHeight?: number;
|
|
44
|
+
/** Gap between cells in px (Figma default 24). */
|
|
45
|
+
gap?: number;
|
|
46
|
+
/** Font family for the digits. Defaults to the theme's regular Inter. */
|
|
47
|
+
fontFamily?: string;
|
|
48
|
+
/** Font size for the digits (Figma default 18). */
|
|
49
|
+
fontSize?: number;
|
|
50
|
+
/** Accessibility label for the hidden field. Defaults to "Code". */
|
|
51
|
+
accessibilityLabel?: string;
|
|
52
|
+
/** Override the container style. */
|
|
53
|
+
style?: StyleProp<ViewStyle>;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Code field — a row of boxes for entering a code (numeric / alpha /
|
|
58
|
+
* alphanumeric, always upper-cased). Backed by a single hidden input, so paste,
|
|
59
|
+
* backspace-to-previous and type-to-next all work natively. The active cell
|
|
60
|
+
* shows the lime focus ring. Cell size, gap, font and font size are configurable.
|
|
61
|
+
*/
|
|
62
|
+
export function CodeField({
|
|
63
|
+
value,
|
|
64
|
+
onChangeText,
|
|
65
|
+
length = 6,
|
|
66
|
+
mode = 'numeric',
|
|
67
|
+
onComplete,
|
|
68
|
+
disabled = false,
|
|
69
|
+
autoFocus = false,
|
|
70
|
+
cellWidth = 126,
|
|
71
|
+
cellHeight = 58,
|
|
72
|
+
gap = 24,
|
|
73
|
+
fontFamily,
|
|
74
|
+
fontSize = 18,
|
|
75
|
+
accessibilityLabel = 'Code',
|
|
76
|
+
style,
|
|
77
|
+
}: CodeFieldProps) {
|
|
78
|
+
const theme = useTheme();
|
|
79
|
+
const inputRef = useRef<TextInput>(null);
|
|
80
|
+
const [focused, setFocused] = useState(false);
|
|
81
|
+
|
|
82
|
+
const handleChange = (text: string) => {
|
|
83
|
+
const next = text.toUpperCase().replace(DISALLOWED[mode], '').slice(0, length);
|
|
84
|
+
onChangeText(next);
|
|
85
|
+
if (next.length === length) onComplete?.(next);
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const activeIndex = Math.min(value.length, length - 1);
|
|
89
|
+
|
|
90
|
+
return (
|
|
91
|
+
<Pressable
|
|
92
|
+
onPress={() => inputRef.current?.focus()}
|
|
93
|
+
disabled={disabled}
|
|
94
|
+
style={[{ position: 'relative', alignSelf: 'flex-start' }, style]}
|
|
95
|
+
>
|
|
96
|
+
<View style={{ flexDirection: 'row', gap }}>
|
|
97
|
+
{Array.from({ length }).map((_, i) => {
|
|
98
|
+
const isActive = focused && i === activeIndex && !disabled;
|
|
99
|
+
return (
|
|
100
|
+
<View
|
|
101
|
+
key={i}
|
|
102
|
+
testID="code-field-cell"
|
|
103
|
+
style={{
|
|
104
|
+
width: cellWidth,
|
|
105
|
+
height: cellHeight,
|
|
106
|
+
borderRadius: theme.radii.pill,
|
|
107
|
+
borderWidth: 1,
|
|
108
|
+
borderColor: isActive ? theme.colors.border.focus : theme.colors.border.default,
|
|
109
|
+
backgroundColor: disabled
|
|
110
|
+
? theme.colors.disabled.background
|
|
111
|
+
: theme.colors.background.default,
|
|
112
|
+
alignItems: 'center',
|
|
113
|
+
justifyContent: 'center',
|
|
114
|
+
opacity: disabled ? 0.6 : 1,
|
|
115
|
+
...(isActive
|
|
116
|
+
? {
|
|
117
|
+
boxShadow: [
|
|
118
|
+
{ offsetX: 0, offsetY: 0, blurRadius: 0, spreadDistance: 6, color: 'rgba(201,255,0,0.15)' },
|
|
119
|
+
] as ViewStyle['boxShadow'],
|
|
120
|
+
}
|
|
121
|
+
: null),
|
|
122
|
+
}}
|
|
123
|
+
>
|
|
124
|
+
<Text
|
|
125
|
+
color={disabled ? 'tertiary' : 'primary'}
|
|
126
|
+
style={{
|
|
127
|
+
fontFamily: fontFamily ?? theme.fontFamily.regular,
|
|
128
|
+
fontSize,
|
|
129
|
+
lineHeight: Math.round(fontSize * 1.45),
|
|
130
|
+
}}
|
|
131
|
+
>
|
|
132
|
+
{value[i] ?? ''}
|
|
133
|
+
</Text>
|
|
134
|
+
</View>
|
|
135
|
+
);
|
|
136
|
+
})}
|
|
137
|
+
</View>
|
|
138
|
+
|
|
139
|
+
<TextInput
|
|
140
|
+
ref={inputRef}
|
|
141
|
+
value={value}
|
|
142
|
+
onChangeText={handleChange}
|
|
143
|
+
onFocus={() => setFocused(true)}
|
|
144
|
+
onBlur={() => setFocused(false)}
|
|
145
|
+
keyboardType={mode === 'numeric' ? 'number-pad' : 'default'}
|
|
146
|
+
autoCapitalize="characters"
|
|
147
|
+
autoCorrect={false}
|
|
148
|
+
maxLength={length}
|
|
149
|
+
editable={!disabled}
|
|
150
|
+
caretHidden
|
|
151
|
+
autoFocus={autoFocus}
|
|
152
|
+
accessibilityLabel={accessibilityLabel}
|
|
153
|
+
style={[
|
|
154
|
+
{ position: 'absolute', top: 0, left: 0, right: 0, bottom: 0, opacity: 0 },
|
|
155
|
+
WEB_NO_OUTLINE,
|
|
156
|
+
]}
|
|
157
|
+
/>
|
|
158
|
+
</Pressable>
|
|
159
|
+
);
|
|
160
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { CodeField, type CodeFieldProps, type CodeFieldMode } from './CodeField';
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { useEffect, useRef } from 'react';
|
|
2
|
+
import { Animated, Easing, Platform, View, type StyleProp, type TextStyle, type ViewStyle } from 'react-native';
|
|
3
|
+
import Svg, { Circle } from 'react-native-svg';
|
|
4
|
+
import { useTheme } from '../../theme';
|
|
5
|
+
import { Text } from '../Text/Text';
|
|
6
|
+
|
|
7
|
+
export type SpinnerSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl';
|
|
8
|
+
|
|
9
|
+
/** Diameters per size — `xl` matches the Figma spinner (164, 12px ring). */
|
|
10
|
+
const SIZES: Record<SpinnerSize, number> = { xs: 24, sm: 40, md: 64, lg: 96, xl: 164 };
|
|
11
|
+
|
|
12
|
+
export interface SpinnerProps {
|
|
13
|
+
/** Size preset: xs / sm / md / lg / xl. Defaults to `md`. */
|
|
14
|
+
size?: SpinnerSize;
|
|
15
|
+
/** Ring color. Defaults to the brand lime action color. */
|
|
16
|
+
color?: string;
|
|
17
|
+
/** Ring thickness in px. Defaults to ~7% of the diameter. */
|
|
18
|
+
thickness?: number;
|
|
19
|
+
/** Optional label shown next to the spinner. */
|
|
20
|
+
label?: string;
|
|
21
|
+
/** Label position relative to the spinner. Defaults to `bottom`. */
|
|
22
|
+
labelPosition?: 'top' | 'bottom';
|
|
23
|
+
/** Distance between spinner and label. Defaults to 8. */
|
|
24
|
+
gap?: number;
|
|
25
|
+
/** Label font size. Defaults to 16. */
|
|
26
|
+
labelSize?: number;
|
|
27
|
+
/** Label color (token or raw). Defaults to secondary text. */
|
|
28
|
+
labelColor?: string;
|
|
29
|
+
/** Rotation duration in ms (one full turn). Defaults to 900. */
|
|
30
|
+
duration?: number;
|
|
31
|
+
style?: StyleProp<ViewStyle>;
|
|
32
|
+
accessibilityLabel?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Spinner — an infinitely rotating ring (react-native-svg + Animated, native
|
|
37
|
+
* driver for smooth motion). Configurable size, color, thickness and an
|
|
38
|
+
* optional label (top/bottom, with adjustable gap and font size).
|
|
39
|
+
*/
|
|
40
|
+
export function Spinner({
|
|
41
|
+
size = 'md',
|
|
42
|
+
color,
|
|
43
|
+
thickness,
|
|
44
|
+
label,
|
|
45
|
+
labelPosition = 'bottom',
|
|
46
|
+
gap = 8,
|
|
47
|
+
labelSize = 16,
|
|
48
|
+
labelColor,
|
|
49
|
+
duration = 900,
|
|
50
|
+
style,
|
|
51
|
+
accessibilityLabel,
|
|
52
|
+
}: SpinnerProps) {
|
|
53
|
+
const theme = useTheme();
|
|
54
|
+
const diameter = SIZES[size];
|
|
55
|
+
const stroke = thickness ?? Math.max(2, Math.round(diameter * 0.073));
|
|
56
|
+
const arcColor = color ?? theme.colors.action.primary;
|
|
57
|
+
const radius = (diameter - stroke) / 2;
|
|
58
|
+
const circumference = 2 * Math.PI * radius;
|
|
59
|
+
|
|
60
|
+
const rotation = useRef(new Animated.Value(0)).current;
|
|
61
|
+
useEffect(() => {
|
|
62
|
+
rotation.setValue(0);
|
|
63
|
+
const loop = Animated.loop(
|
|
64
|
+
Animated.timing(rotation, {
|
|
65
|
+
toValue: 1,
|
|
66
|
+
duration,
|
|
67
|
+
easing: Easing.linear,
|
|
68
|
+
// Native driver loops smoothly on iOS/Android; on web (react-native-web)
|
|
69
|
+
// a native-driven loop only runs once, so drive it on the JS thread there.
|
|
70
|
+
useNativeDriver: Platform.OS !== 'web',
|
|
71
|
+
}),
|
|
72
|
+
);
|
|
73
|
+
loop.start();
|
|
74
|
+
return () => loop.stop();
|
|
75
|
+
}, [rotation, duration]);
|
|
76
|
+
|
|
77
|
+
const spin = rotation.interpolate({ inputRange: [0, 1], outputRange: ['0deg', '360deg'] });
|
|
78
|
+
|
|
79
|
+
const labelStyle: TextStyle = {
|
|
80
|
+
fontSize: labelSize,
|
|
81
|
+
lineHeight: Math.round(labelSize * 1.4),
|
|
82
|
+
...(labelPosition === 'top' ? {} : null),
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
return (
|
|
86
|
+
<View
|
|
87
|
+
accessibilityRole="progressbar"
|
|
88
|
+
accessibilityLabel={accessibilityLabel ?? label}
|
|
89
|
+
style={[
|
|
90
|
+
{
|
|
91
|
+
alignItems: 'center',
|
|
92
|
+
gap,
|
|
93
|
+
flexDirection: labelPosition === 'top' ? 'column-reverse' : 'column',
|
|
94
|
+
},
|
|
95
|
+
style,
|
|
96
|
+
]}
|
|
97
|
+
>
|
|
98
|
+
<Animated.View style={{ width: diameter, height: diameter, transform: [{ rotate: spin }] }}>
|
|
99
|
+
<Svg width={diameter} height={diameter}>
|
|
100
|
+
<Circle
|
|
101
|
+
cx={diameter / 2}
|
|
102
|
+
cy={diameter / 2}
|
|
103
|
+
r={radius}
|
|
104
|
+
stroke={arcColor}
|
|
105
|
+
strokeWidth={stroke}
|
|
106
|
+
fill="none"
|
|
107
|
+
opacity={0.2}
|
|
108
|
+
/>
|
|
109
|
+
<Circle
|
|
110
|
+
cx={diameter / 2}
|
|
111
|
+
cy={diameter / 2}
|
|
112
|
+
r={radius}
|
|
113
|
+
stroke={arcColor}
|
|
114
|
+
strokeWidth={stroke}
|
|
115
|
+
fill="none"
|
|
116
|
+
strokeLinecap="round"
|
|
117
|
+
strokeDasharray={[circumference * 0.7, circumference * 0.3]}
|
|
118
|
+
/>
|
|
119
|
+
</Svg>
|
|
120
|
+
</Animated.View>
|
|
121
|
+
{label ? (
|
|
122
|
+
<Text color={labelColor ?? 'secondary'} style={labelStyle}>
|
|
123
|
+
{label}
|
|
124
|
+
</Text>
|
|
125
|
+
) : null}
|
|
126
|
+
</View>
|
|
127
|
+
);
|
|
128
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { Spinner, type SpinnerProps, type SpinnerSize } from './Spinner';
|