@arcblock/ux 2.12.15 → 2.12.16

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.
@@ -0,0 +1,13 @@
1
+ import { SelectProps } from '@mui/material';
2
+ import type { CountryIso2 } from 'react-international-phone';
3
+ export interface CountryDisplayOptions {
4
+ showFullName?: boolean;
5
+ }
6
+ export interface CountrySelectProps extends Omit<SelectProps, 'value' | 'onChange'> {
7
+ value: CountryIso2;
8
+ onChange?: (value: CountryIso2) => void;
9
+ selectCountryProps?: CountryDisplayOptions;
10
+ preview?: boolean;
11
+ }
12
+ declare const CountrySelect: import("react").ForwardRefExoticComponent<Omit<CountrySelectProps, "ref"> & import("react").RefAttributes<HTMLDivElement>>;
13
+ export default CountrySelect;
@@ -0,0 +1,142 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useMemo, forwardRef, useState, useCallback } from 'react';
3
+ import { Box, MenuItem, Select, Typography, TextField } from '@mui/material';
4
+ import { FlagEmoji, defaultCountries, parseCountry } from 'react-international-phone';
5
+ const CountrySelect = /*#__PURE__*/forwardRef(({
6
+ value,
7
+ onChange,
8
+ sx = {},
9
+ selectCountryProps,
10
+ preview = false
11
+ }, ref) => {
12
+ const {
13
+ showFullName = false
14
+ } = selectCountryProps || {};
15
+ const [searchQuery, setSearchQuery] = useState('');
16
+ const countryDetail = useMemo(() => {
17
+ const item = defaultCountries.find(v => v[1] === value);
18
+ return value && item ? parseCountry(item) : {
19
+ name: '',
20
+ iso2: ''
21
+ };
22
+ }, [value]);
23
+ const filteredCountries = useMemo(() => {
24
+ if (!searchQuery) return defaultCountries;
25
+ const query = searchQuery.toLowerCase();
26
+ return defaultCountries.filter(country => {
27
+ const parsed = parseCountry(country);
28
+ return parsed.name.toLowerCase().includes(query) || parsed.iso2.toLowerCase().includes(query) || parsed.dialCode.includes(query);
29
+ });
30
+ }, [searchQuery]);
31
+ const onCountryChange = e => {
32
+ onChange?.(e.target.value);
33
+ };
34
+ const renderCountryContent = useCallback(code => /*#__PURE__*/_jsxs(Box, {
35
+ display: "flex",
36
+ alignItems: "center",
37
+ flexWrap: "nowrap",
38
+ gap: 0.5,
39
+ sx: {
40
+ cursor: preview ? 'default' : 'pointer'
41
+ },
42
+ children: [/*#__PURE__*/_jsx(FlagEmoji, {
43
+ iso2: code,
44
+ style: {
45
+ display: 'flex',
46
+ width: '24px',
47
+ color: 'inherit'
48
+ }
49
+ }), /*#__PURE__*/_jsx(Typography, {
50
+ children: showFullName ? countryDetail?.name : countryDetail?.iso2
51
+ })]
52
+ }), [preview, showFullName, countryDetail]);
53
+ if (preview) {
54
+ return renderCountryContent(value);
55
+ }
56
+ return /*#__PURE__*/_jsxs(Select, {
57
+ ref: ref,
58
+ MenuProps: {
59
+ style: {
60
+ height: '300px',
61
+ top: '10px'
62
+ },
63
+ anchorOrigin: {
64
+ vertical: 'bottom',
65
+ horizontal: 'left'
66
+ },
67
+ transformOrigin: {
68
+ vertical: 'top',
69
+ horizontal: 'left'
70
+ }
71
+ },
72
+ sx: {
73
+ width: '100%',
74
+ fieldset: {
75
+ display: 'none'
76
+ },
77
+ '&.Mui-focused:has(div[aria-expanded="false"])': {
78
+ fieldset: {
79
+ display: 'block'
80
+ }
81
+ },
82
+ '.MuiSelect-select': {
83
+ padding: '8px',
84
+ paddingRight: '24px !important'
85
+ },
86
+ svg: {
87
+ right: 0
88
+ },
89
+ '.MuiMenuItem-root': {
90
+ justifyContent: 'flex-start'
91
+ },
92
+ ...sx
93
+ },
94
+ value: value,
95
+ onChange: onCountryChange,
96
+ renderValue: renderCountryContent,
97
+ children: [/*#__PURE__*/_jsx(Box, {
98
+ sx: {
99
+ p: 1,
100
+ position: 'sticky',
101
+ top: 0,
102
+ bgcolor: 'background.paper',
103
+ zIndex: 1
104
+ },
105
+ children: /*#__PURE__*/_jsx(TextField, {
106
+ size: "small",
107
+ fullWidth: true,
108
+ placeholder: "Search country...",
109
+ value: searchQuery,
110
+ onChange: e => setSearchQuery(e.target.value),
111
+ onClick: e => e.stopPropagation(),
112
+ onKeyDown: e => e.stopPropagation(),
113
+ sx: {
114
+ '& .MuiOutlinedInput-root': {
115
+ '& fieldset': {
116
+ borderColor: 'divider'
117
+ }
118
+ }
119
+ }
120
+ })
121
+ }), filteredCountries.map(c => {
122
+ const parsed = parseCountry(c);
123
+ return /*#__PURE__*/_jsxs(MenuItem, {
124
+ value: parsed.iso2,
125
+ children: [/*#__PURE__*/_jsx(FlagEmoji, {
126
+ iso2: parsed.iso2,
127
+ style: {
128
+ marginRight: '8px',
129
+ width: '24px'
130
+ }
131
+ }), /*#__PURE__*/_jsx(Typography, {
132
+ marginRight: "8px",
133
+ children: parsed.name
134
+ }), /*#__PURE__*/_jsxs(Typography, {
135
+ color: "gray",
136
+ children: ["+", parsed.dialCode]
137
+ })]
138
+ }, parsed.iso2);
139
+ })]
140
+ });
141
+ });
142
+ export default CountrySelect;
@@ -0,0 +1,17 @@
1
+ import { TextFieldProps } from '@mui/material';
2
+ import { CountryIso2 } from 'react-international-phone';
3
+ import { CountryDisplayOptions } from './country-select';
4
+ export interface PhoneValue {
5
+ country: CountryIso2;
6
+ phone: string;
7
+ }
8
+ export interface PhoneInputProps extends Omit<TextFieldProps, 'value' | 'onChange'> {
9
+ value?: PhoneValue;
10
+ onChange?: (value: PhoneValue) => void;
11
+ countryDisplayOptions?: CountryDisplayOptions;
12
+ preview?: boolean;
13
+ allowDial?: boolean;
14
+ }
15
+ export declare function getCountryCallingCode(country: CountryIso2): string;
16
+ export declare function validatePhoneNumber(phone: string): boolean;
17
+ export default function PhoneInput({ value, onChange, placeholder, countryDisplayOptions, preview, allowDial, ...props }: PhoneInputProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,137 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useMemo } from 'react';
3
+ import { Box, InputAdornment, TextField, Typography } from '@mui/material';
4
+ import { defaultCountries, usePhoneInput, parseCountry } from 'react-international-phone';
5
+ import isMobilePhone from 'validator/lib/isMobilePhone';
6
+ import CountrySelect from './country-select';
7
+ export function getCountryCallingCode(country) {
8
+ const countryData = defaultCountries.find(c => parseCountry(c).iso2 === country);
9
+ if (countryData) {
10
+ return parseCountry(countryData).dialCode.replace('+', '');
11
+ }
12
+ return '';
13
+ }
14
+ export function validatePhoneNumber(phone) {
15
+ return isMobilePhone(phone.replace(/[\s\-()]+/g, ''), 'any', {
16
+ strictMode: true
17
+ });
18
+ }
19
+ export default function PhoneInput({
20
+ value = {
21
+ country: 'us',
22
+ phone: ''
23
+ },
24
+ onChange,
25
+ placeholder = 'Enter phone number',
26
+ countryDisplayOptions = {},
27
+ preview = false,
28
+ allowDial = true,
29
+ ...props
30
+ }) {
31
+ // 在使用 usePhoneInput 之前处理电话号码
32
+ const phoneWithPrefix = useMemo(() => {
33
+ if (!value.phone) return '';
34
+ // 如果电话号码已经包含 + 号,说明已经有区号了
35
+ if (value.phone.startsWith('+')) return value.phone;
36
+ // 获取国家区号并添加到号码前
37
+ const countryCode = getCountryCallingCode(value.country);
38
+ return `+${countryCode}${value.phone}`;
39
+ }, [value.phone, value.country]);
40
+ const {
41
+ phone,
42
+ handlePhoneValueChange,
43
+ country,
44
+ setCountry
45
+ } = usePhoneInput({
46
+ defaultCountry: value.country,
47
+ value: phoneWithPrefix,
48
+ countries: defaultCountries,
49
+ onChange: data => {
50
+ // 确保类型匹配 PhoneValue 接口
51
+ const phoneValue = {
52
+ country: data.country,
53
+ phone: data.phone
54
+ };
55
+ onChange?.(phoneValue);
56
+ }
57
+ });
58
+ const onCountryChange = v => {
59
+ if (!preview) {
60
+ setCountry(v);
61
+ }
62
+ };
63
+ if (preview) {
64
+ const isValid = phone && validatePhoneNumber(phone);
65
+ const canDial = allowDial && isValid;
66
+ return /*#__PURE__*/_jsxs(Box, {
67
+ display: "flex",
68
+ alignItems: "center",
69
+ component: canDial ? 'a' : 'div',
70
+ href: canDial ? `tel:${phone}` : undefined,
71
+ sx: {
72
+ textDecoration: 'none',
73
+ color: 'inherit',
74
+ cursor: canDial ? 'pointer' : 'default',
75
+ '&:hover': {
76
+ opacity: canDial ? 0.8 : 1
77
+ }
78
+ },
79
+ children: [/*#__PURE__*/_jsx(CountrySelect, {
80
+ value: country,
81
+ preview: preview
82
+ }), /*#__PURE__*/_jsx(Typography, {
83
+ sx: {
84
+ ml: 0.5
85
+ },
86
+ children: phone
87
+ })]
88
+ });
89
+ }
90
+ return /*#__PURE__*/_jsx(TextField, {
91
+ ...props,
92
+ value: phone,
93
+ onChange: preview ? undefined : handlePhoneValueChange,
94
+ placeholder: placeholder,
95
+ className: "phone-input",
96
+ disabled: preview,
97
+ sx: {
98
+ '&>.MuiInputBase-root': {
99
+ paddingLeft: '8px',
100
+ cursor: preview ? 'default' : undefined
101
+ },
102
+ ...(props.sx ?? {}),
103
+ ...(preview ? {
104
+ '& .Mui-disabled': {
105
+ '-webkit-text-fill-color': 'inherit',
106
+ color: 'inherit'
107
+ },
108
+ fieldset: {
109
+ display: 'none'
110
+ }
111
+ } : {})
112
+ },
113
+ InputProps: {
114
+ startAdornment: /*#__PURE__*/_jsx(InputAdornment, {
115
+ position: "start",
116
+ style: {
117
+ marginRight: '2px',
118
+ marginLeft: '-8px'
119
+ },
120
+ children: /*#__PURE__*/_jsx(CountrySelect, {
121
+ value: country,
122
+ onChange: onCountryChange,
123
+ preview: preview,
124
+ selectCountryProps: countryDisplayOptions,
125
+ sx: {
126
+ '&.Mui-focused .MuiOutlinedInput-notchedOutline': {
127
+ borderColor: 'transparent'
128
+ },
129
+ opacity: preview ? 1 : undefined,
130
+ cursor: preview ? 'default' : undefined
131
+ }
132
+ })
133
+ }),
134
+ ...(props.InputProps ?? {})
135
+ }
136
+ });
137
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arcblock/ux",
3
- "version": "2.12.15",
3
+ "version": "2.12.16",
4
4
  "description": "Common used react components for arcblock products",
5
5
  "keywords": [
6
6
  "react",
@@ -68,12 +68,12 @@
68
68
  "react": ">=18.2.0",
69
69
  "react-router-dom": ">=6.22.3"
70
70
  },
71
- "gitHead": "16a4f35c677fc2ca2118801dc62984b71734df70",
71
+ "gitHead": "02b619f243bf6a0c6847349edf513e18b212bcd7",
72
72
  "dependencies": {
73
73
  "@arcblock/did-motif": "^1.1.13",
74
- "@arcblock/icons": "^2.12.15",
75
- "@arcblock/nft-display": "^2.12.15",
76
- "@arcblock/react-hooks": "^2.12.15",
74
+ "@arcblock/icons": "^2.12.16",
75
+ "@arcblock/nft-display": "^2.12.16",
76
+ "@arcblock/react-hooks": "^2.12.16",
77
77
  "@babel/plugin-syntax-dynamic-import": "^7.8.3",
78
78
  "@fontsource/inter": "^5.0.16",
79
79
  "@fontsource/ubuntu-mono": "^5.0.18",
@@ -110,6 +110,7 @@
110
110
  "react-error-boundary": "^3.1.4",
111
111
  "react-ga4": "^2.1.0",
112
112
  "react-helmet": "^6.1.0",
113
+ "react-international-phone": "^3.1.2",
113
114
  "react-intersection-observer": "^8.34.0",
114
115
  "react-lottie-player": "^1.4.3",
115
116
  "react-player": "^1.15.3",
@@ -119,6 +120,7 @@
119
120
  "rebound": "^0.1.0",
120
121
  "topojson-client": "^3.1.0",
121
122
  "type-fest": "^4.28.0",
123
+ "validator": "^13.9.0",
122
124
  "versor": "^0.0.4"
123
125
  }
124
126
  }
@@ -0,0 +1,138 @@
1
+ import { useMemo, forwardRef, useState, useCallback } from 'react';
2
+ import { Box, MenuItem, Select, Typography, SelectProps, TextField } from '@mui/material';
3
+ import { FlagEmoji, defaultCountries, parseCountry } from 'react-international-phone';
4
+ import type { CountryIso2 } from 'react-international-phone';
5
+
6
+ export interface CountryDisplayOptions {
7
+ showFullName?: boolean;
8
+ }
9
+
10
+ export interface CountrySelectProps extends Omit<SelectProps, 'value' | 'onChange'> {
11
+ value: CountryIso2;
12
+ onChange?: (value: CountryIso2) => void;
13
+ selectCountryProps?: CountryDisplayOptions;
14
+ preview?: boolean;
15
+ }
16
+
17
+ const CountrySelect = forwardRef<HTMLDivElement, CountrySelectProps>(
18
+ ({ value, onChange, sx = {}, selectCountryProps, preview = false }, ref) => {
19
+ const { showFullName = false } = selectCountryProps || {};
20
+ const [searchQuery, setSearchQuery] = useState('');
21
+
22
+ const countryDetail = useMemo(() => {
23
+ const item = defaultCountries.find((v) => v[1] === value);
24
+ return value && item ? parseCountry(item) : { name: '', iso2: '' };
25
+ }, [value]);
26
+
27
+ const filteredCountries = useMemo(() => {
28
+ if (!searchQuery) return defaultCountries;
29
+ const query = searchQuery.toLowerCase();
30
+ return defaultCountries.filter((country) => {
31
+ const parsed = parseCountry(country);
32
+ return (
33
+ parsed.name.toLowerCase().includes(query) ||
34
+ parsed.iso2.toLowerCase().includes(query) ||
35
+ parsed.dialCode.includes(query)
36
+ );
37
+ });
38
+ }, [searchQuery]);
39
+
40
+ const onCountryChange = (e: any) => {
41
+ onChange?.(e.target.value as CountryIso2);
42
+ };
43
+
44
+ const renderCountryContent = useCallback(
45
+ (code: CountryIso2) => (
46
+ <Box
47
+ display="flex"
48
+ alignItems="center"
49
+ flexWrap="nowrap"
50
+ gap={0.5}
51
+ sx={{ cursor: preview ? 'default' : 'pointer' }}>
52
+ <FlagEmoji iso2={code} style={{ display: 'flex', width: '24px', color: 'inherit' }} />
53
+ <Typography>{showFullName ? countryDetail?.name : countryDetail?.iso2}</Typography>
54
+ </Box>
55
+ ),
56
+ [preview, showFullName, countryDetail]
57
+ );
58
+
59
+ if (preview) {
60
+ return renderCountryContent(value);
61
+ }
62
+
63
+ return (
64
+ <Select
65
+ ref={ref}
66
+ MenuProps={{
67
+ style: {
68
+ height: '300px',
69
+ top: '10px',
70
+ },
71
+ anchorOrigin: {
72
+ vertical: 'bottom',
73
+ horizontal: 'left',
74
+ },
75
+ transformOrigin: {
76
+ vertical: 'top',
77
+ horizontal: 'left',
78
+ },
79
+ }}
80
+ sx={{
81
+ width: '100%',
82
+ fieldset: {
83
+ display: 'none',
84
+ },
85
+ '&.Mui-focused:has(div[aria-expanded="false"])': {
86
+ fieldset: {
87
+ display: 'block',
88
+ },
89
+ },
90
+ '.MuiSelect-select': {
91
+ padding: '8px',
92
+ paddingRight: '24px !important',
93
+ },
94
+ svg: {
95
+ right: 0,
96
+ },
97
+ '.MuiMenuItem-root': {
98
+ justifyContent: 'flex-start',
99
+ },
100
+ ...sx,
101
+ }}
102
+ value={value}
103
+ onChange={onCountryChange}
104
+ renderValue={renderCountryContent}>
105
+ <Box sx={{ p: 1, position: 'sticky', top: 0, bgcolor: 'background.paper', zIndex: 1 }}>
106
+ <TextField
107
+ size="small"
108
+ fullWidth
109
+ placeholder="Search country..."
110
+ value={searchQuery}
111
+ onChange={(e) => setSearchQuery(e.target.value)}
112
+ onClick={(e) => e.stopPropagation()}
113
+ onKeyDown={(e) => e.stopPropagation()}
114
+ sx={{
115
+ '& .MuiOutlinedInput-root': {
116
+ '& fieldset': {
117
+ borderColor: 'divider',
118
+ },
119
+ },
120
+ }}
121
+ />
122
+ </Box>
123
+ {filteredCountries.map((c: any) => {
124
+ const parsed = parseCountry(c);
125
+ return (
126
+ <MenuItem key={parsed.iso2} value={parsed.iso2}>
127
+ <FlagEmoji iso2={parsed.iso2} style={{ marginRight: '8px', width: '24px' }} />
128
+ <Typography marginRight="8px">{parsed.name}</Typography>
129
+ <Typography color="gray">+{parsed.dialCode}</Typography>
130
+ </MenuItem>
131
+ );
132
+ })}
133
+ </Select>
134
+ );
135
+ }
136
+ );
137
+
138
+ export default CountrySelect;
@@ -0,0 +1,142 @@
1
+ import { useMemo } from 'react';
2
+ import { Box, InputAdornment, TextField, TextFieldProps, Typography } from '@mui/material';
3
+ import { defaultCountries, CountryIso2, usePhoneInput, parseCountry } from 'react-international-phone';
4
+ import isMobilePhone from 'validator/lib/isMobilePhone';
5
+ import CountrySelect, { CountryDisplayOptions } from './country-select';
6
+
7
+ export interface PhoneValue {
8
+ country: CountryIso2;
9
+ phone: string;
10
+ }
11
+
12
+ export interface PhoneInputProps extends Omit<TextFieldProps, 'value' | 'onChange'> {
13
+ value?: PhoneValue;
14
+ onChange?: (value: PhoneValue) => void;
15
+ countryDisplayOptions?: CountryDisplayOptions;
16
+ preview?: boolean;
17
+ allowDial?: boolean; // 是否允许拨号, 只在 preview 为 true 时有效
18
+ }
19
+
20
+ export function getCountryCallingCode(country: CountryIso2): string {
21
+ const countryData = defaultCountries.find((c) => parseCountry(c).iso2 === country);
22
+ if (countryData) {
23
+ return parseCountry(countryData).dialCode.replace('+', '');
24
+ }
25
+ return '';
26
+ }
27
+
28
+ export function validatePhoneNumber(phone: string): boolean {
29
+ return isMobilePhone(phone.replace(/[\s\-()]+/g, ''), 'any', { strictMode: true });
30
+ }
31
+
32
+ export default function PhoneInput({
33
+ value = { country: 'us', phone: '' },
34
+ onChange,
35
+ placeholder = 'Enter phone number',
36
+ countryDisplayOptions = {},
37
+ preview = false,
38
+ allowDial = true,
39
+ ...props
40
+ }: PhoneInputProps) {
41
+ // 在使用 usePhoneInput 之前处理电话号码
42
+ const phoneWithPrefix = useMemo(() => {
43
+ if (!value.phone) return '';
44
+ // 如果电话号码已经包含 + 号,说明已经有区号了
45
+ if (value.phone.startsWith('+')) return value.phone;
46
+ // 获取国家区号并添加到号码前
47
+ const countryCode = getCountryCallingCode(value.country);
48
+ return `+${countryCode}${value.phone}`;
49
+ }, [value.phone, value.country]);
50
+
51
+ const { phone, handlePhoneValueChange, country, setCountry } = usePhoneInput({
52
+ defaultCountry: value.country,
53
+ value: phoneWithPrefix,
54
+ countries: defaultCountries,
55
+ onChange: (data) => {
56
+ // 确保类型匹配 PhoneValue 接口
57
+ const phoneValue: PhoneValue = {
58
+ country: data.country,
59
+ phone: data.phone,
60
+ };
61
+ onChange?.(phoneValue);
62
+ },
63
+ });
64
+
65
+ const onCountryChange = (v: CountryIso2) => {
66
+ if (!preview) {
67
+ setCountry(v);
68
+ }
69
+ };
70
+
71
+ if (preview) {
72
+ const isValid = phone && validatePhoneNumber(phone);
73
+ const canDial = allowDial && isValid;
74
+
75
+ return (
76
+ <Box
77
+ display="flex"
78
+ alignItems="center"
79
+ component={canDial ? 'a' : 'div'}
80
+ href={canDial ? `tel:${phone}` : undefined}
81
+ sx={{
82
+ textDecoration: 'none',
83
+ color: 'inherit',
84
+ cursor: canDial ? 'pointer' : 'default',
85
+ '&:hover': {
86
+ opacity: canDial ? 0.8 : 1,
87
+ },
88
+ }}>
89
+ <CountrySelect value={country} preview={preview} />
90
+ <Typography sx={{ ml: 0.5 }}>{phone}</Typography>
91
+ </Box>
92
+ );
93
+ }
94
+ return (
95
+ <TextField
96
+ {...props}
97
+ value={phone}
98
+ onChange={preview ? undefined : handlePhoneValueChange}
99
+ placeholder={placeholder}
100
+ className="phone-input"
101
+ disabled={preview}
102
+ sx={{
103
+ '&>.MuiInputBase-root': {
104
+ paddingLeft: '8px',
105
+ cursor: preview ? 'default' : undefined,
106
+ },
107
+ ...(props.sx ?? {}),
108
+ ...(preview
109
+ ? {
110
+ '& .Mui-disabled': {
111
+ '-webkit-text-fill-color': 'inherit',
112
+ color: 'inherit',
113
+ },
114
+ fieldset: {
115
+ display: 'none',
116
+ },
117
+ }
118
+ : {}),
119
+ }}
120
+ InputProps={{
121
+ startAdornment: (
122
+ <InputAdornment position="start" style={{ marginRight: '2px', marginLeft: '-8px' }}>
123
+ <CountrySelect
124
+ value={country}
125
+ onChange={onCountryChange}
126
+ preview={preview}
127
+ selectCountryProps={countryDisplayOptions}
128
+ sx={{
129
+ '&.Mui-focused .MuiOutlinedInput-notchedOutline': {
130
+ borderColor: 'transparent',
131
+ },
132
+ opacity: preview ? 1 : undefined,
133
+ cursor: preview ? 'default' : undefined,
134
+ }}
135
+ />
136
+ </InputAdornment>
137
+ ),
138
+ ...(props.InputProps ?? {}),
139
+ }}
140
+ />
141
+ );
142
+ }