@arcblock/ux 1.16.0 → 1.16.4

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 (90) hide show
  1. package/lib/CodeBlock/index.js +3 -1
  2. package/package.json +6 -5
  3. package/src/ActionButton/index.js +65 -0
  4. package/src/ActivityIndicator/index.js +186 -0
  5. package/src/Alert/index.js +104 -0
  6. package/src/Async/index.js +39 -0
  7. package/src/Badge/index.js +71 -0
  8. package/src/Blocklet/index.js +335 -0
  9. package/src/Button/index.js +4 -0
  10. package/src/Button/wrap.js +88 -0
  11. package/src/ButtonGroup/index.js +19 -0
  12. package/src/Center/index.js +17 -0
  13. package/src/ClickToCopy/index.js +90 -0
  14. package/src/CodeBlock/index.js +160 -0
  15. package/src/Colors/index.js +1 -0
  16. package/src/Colors/themes/default.js +53 -0
  17. package/src/ContactForm/index.js +240 -0
  18. package/src/CookieConsent/index.js +90 -0
  19. package/src/CountDown/index.js +151 -0
  20. package/src/Dialog/confirm.js +76 -0
  21. package/src/Dialog/dialog.js +162 -0
  22. package/src/Dialog/index.js +2 -0
  23. package/src/DriftBot/index.js +81 -0
  24. package/src/Earth/countries.json +8057 -0
  25. package/src/Earth/index.js +511 -0
  26. package/src/Earth/util.js +69 -0
  27. package/src/Empty/index.js +41 -0
  28. package/src/Footer/index.js +84 -0
  29. package/src/Icon/image.js +55 -0
  30. package/src/Icon/index.js +69 -0
  31. package/src/Img/index.js +172 -0
  32. package/src/InfoRow/index.js +83 -0
  33. package/src/Layout/dashboard/header.js +145 -0
  34. package/src/Layout/dashboard/index.js +140 -0
  35. package/src/Layout/dashboard/sidebar.js +120 -0
  36. package/src/Layout/index.js +318 -0
  37. package/src/Locale/browser-lang.js +63 -0
  38. package/src/Locale/context.js +88 -0
  39. package/src/Locale/images/globe-dark.png +0 -0
  40. package/src/Locale/images/globe-light.png +0 -0
  41. package/src/Locale/selector.js +138 -0
  42. package/src/Logo/images/logo-dark-text.svg +3 -0
  43. package/src/Logo/images/logo-dark-top.svg +6 -0
  44. package/src/Logo/images/logo-light-text.svg +3 -0
  45. package/src/Logo/images/logo-light-top.svg +6 -0
  46. package/src/Logo/index.js +47 -0
  47. package/src/Metric/index.js +115 -0
  48. package/src/NFTDisplay/README.md +59 -0
  49. package/src/NFTDisplay/aspect-ratio-container.js +34 -0
  50. package/src/NFTDisplay/broken.js +18 -0
  51. package/src/NFTDisplay/index.js +230 -0
  52. package/src/NFTDisplay/loading.js +17 -0
  53. package/src/NFTDisplay/svg-embedder/img.js +36 -0
  54. package/src/NFTDisplay/svg-embedder/inline-svg.js +37 -0
  55. package/src/PageScroller/index.js +342 -0
  56. package/src/PageScroller/usePrevValue.js +12 -0
  57. package/src/PricingTable/PricingPlan.js +112 -0
  58. package/src/PricingTable/index.js +43 -0
  59. package/src/Screenshot/devices.css +1366 -0
  60. package/src/Screenshot/index.js +181 -0
  61. package/src/Spinner/index.js +33 -0
  62. package/src/Switch/index.js +78 -0
  63. package/src/Tabs/index.js +46 -0
  64. package/src/Tag/index.js +73 -0
  65. package/src/Terminal/Player.js +364 -0
  66. package/src/Terminal/index.js +150 -0
  67. package/src/Terminal/player.css +378 -0
  68. package/src/Terminal/util.js +167 -0
  69. package/src/Terminal/xterm.css +171 -0
  70. package/src/TextCollapse/index.js +92 -0
  71. package/src/Theme/index.js +169 -0
  72. package/src/Theme/responsiveFontSizes.js +94 -0
  73. package/src/Toast/index.js +118 -0
  74. package/src/Util/index.js +264 -0
  75. package/src/Video/index.js +72 -0
  76. package/src/Wallet/Action.js +105 -0
  77. package/src/Wallet/Download.js +130 -0
  78. package/src/Wallet/Open.js +50 -0
  79. package/src/Wallet/images/abtwallet.png +0 -0
  80. package/src/Wallet/images/android_download.svg +23 -0
  81. package/src/Wallet/images/app-store.svg +20 -0
  82. package/src/Wallet/images/google-play.svg +70 -0
  83. package/src/WechatPrompt/images/android.png +0 -0
  84. package/src/WechatPrompt/images/ios.png +0 -0
  85. package/src/WechatPrompt/index.js +81 -0
  86. package/src/index.js +63 -0
  87. package/src/withTheme/index.js +72 -0
  88. package/src/withTracker/README.md +34 -0
  89. package/src/withTracker/error_boundary.js +34 -0
  90. package/src/withTracker/index.js +70 -0
@@ -0,0 +1,138 @@
1
+ import React, { useState, useContext } from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import styled from 'styled-components';
4
+
5
+ import { useTheme } from '@material-ui/core/styles';
6
+ import Button from '@material-ui/core/Button';
7
+ import Typography from '@material-ui/core/Typography';
8
+ import IconButton from '@material-ui/core/IconButton';
9
+ import ClickAwayListener from '@material-ui/core/ClickAwayListener';
10
+ import Popper from '@material-ui/core/Popper';
11
+ import MenuItem from '@material-ui/core/MenuItem';
12
+ import MenuList from '@material-ui/core/MenuList';
13
+ import CheckIcon from '@material-ui/icons/Check';
14
+
15
+ import { getColor, getBackground } from '../Util';
16
+
17
+ import { LocaleContext, languages } from './context';
18
+
19
+ import globeDark from './images/globe-dark.png';
20
+ import globeLight from './images/globe-light.png';
21
+
22
+ function LocaleSelector(props) {
23
+ const { dark, showText, popperProps, ...rest } = props;
24
+ const { locale, changeLocale } = useContext(LocaleContext);
25
+ const [anchorEl, setAnchorEl] = useState(null);
26
+ const [open, setOpen] = useState(false);
27
+ const theme = useTheme();
28
+
29
+ const onSelect = newLocale => {
30
+ changeLocale(newLocale);
31
+ setOpen(false);
32
+ if (typeof props.onChange === 'function') {
33
+ props.onChange(newLocale);
34
+ }
35
+ };
36
+
37
+ function onClose(e) {
38
+ if (anchorEl && anchorEl.contains(e.target)) return;
39
+ setOpen(false);
40
+ }
41
+
42
+ const ButtonComponent = showText ? Button : IconButton;
43
+
44
+ return (
45
+ <Div component="div" dark={dark} theme={theme} {...rest}>
46
+ <ButtonComponent
47
+ buttonRef={node => setAnchorEl(node)}
48
+ className="trigger"
49
+ onClick={() => setOpen(!open)}>
50
+ <img src={dark ? globeDark : globeLight} className="trigger-image" alt="globe" />
51
+ {showText ? (
52
+ <Typography component="strong" className="trigger-text">
53
+ {languages.find(x => x.value === locale).text}
54
+ </Typography>
55
+ ) : (
56
+ ''
57
+ )}
58
+ </ButtonComponent>
59
+ <Popper open={open} anchorEl={anchorEl} {...popperProps} disablePortal>
60
+ <div className="locales">
61
+ <ClickAwayListener onClickAway={onClose}>
62
+ <MenuList>
63
+ {languages.map(({ value, text }) => (
64
+ <MenuItem key={value} className="locale-item" onClick={() => onSelect(value, text)}>
65
+ <CheckIcon
66
+ className={value === locale ? 'check-icon check-icon-visible' : 'check-icon'}
67
+ fontSize="small"
68
+ />
69
+ {text}
70
+ </MenuItem>
71
+ ))}
72
+ </MenuList>
73
+ </ClickAwayListener>
74
+ </div>
75
+ </Popper>
76
+ </Div>
77
+ );
78
+ }
79
+
80
+ LocaleSelector.propTypes = {
81
+ dark: PropTypes.bool,
82
+ size: PropTypes.number,
83
+ showText: PropTypes.bool,
84
+ popperProps: PropTypes.object,
85
+ onChange: PropTypes.func,
86
+ };
87
+
88
+ LocaleSelector.defaultProps = {
89
+ dark: false,
90
+ showText: true,
91
+ size: 24,
92
+ popperProps: {},
93
+ onChange: () => {},
94
+ };
95
+
96
+ export default LocaleSelector;
97
+
98
+ const Div = styled.div`
99
+ .trigger {
100
+ display: flex;
101
+ flex-direction: column;
102
+ justify-content: center;
103
+ font-size: 14px;
104
+ .trigger-image {
105
+ width: ${props => props.size}px;
106
+ height: ${props => props.size}px;
107
+ }
108
+ .trigger-text {
109
+ margin-left: 5px;
110
+ font-size: 14px;
111
+ }
112
+ }
113
+
114
+ .locales {
115
+ background: ${props => getBackground(props)};
116
+ }
117
+
118
+ .locale-item {
119
+ font-size: 16px;
120
+ font-style: normal;
121
+ font-stretch: normal;
122
+ line-height: normal;
123
+ letter-spacing: 2px;
124
+ text-align: center;
125
+ color: ${props => getColor(props)};
126
+ cursor: pointer;
127
+ display: flex;
128
+ padding: 16px;
129
+ align-items: center;
130
+ .check-icon {
131
+ visibility: hidden;
132
+ margin-right: 4px;
133
+ }
134
+ .check-icon-visible {
135
+ visibility: visible;
136
+ }
137
+ }
138
+ `;
@@ -0,0 +1,3 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="99" height="18" viewBox="0 0 99 18">
2
+ <path fill="#000" fill-rule="evenodd" d="M6.5.053h2.864L16.028 18h-2.114l-1.391-3.795H3.504L2.14 18H0L6.5.053zm1.474 1.481L4.04 12.563h7.895L7.975 1.534zm14.44 3.542c1.27 0 2.198.296 2.78.892.58.597.871 1.52.871 2.765v.36c0 .117-.008.245-.026.385h-1.826c.018-.14.027-.292.027-.452v-.48c0-.766-.186-1.283-.551-1.55-.367-.266-.916-.4-1.65-.4-.968 0-1.698.24-2.189.723-.493.48-.738 1.208-.738 2.187v8.308h-1.88V5.341h1.824l-.079 1.443c.376-.614.863-1.053 1.463-1.316a4.868 4.868 0 0 1 1.974-.392zM35.95 15.903c.501-.328.752-1.205.752-2.63h1.852c0 1.924-.403 3.178-1.208 3.765-.806.589-2.158.882-4.055.882-2.65 0-4.24-.538-4.766-1.616-.529-1.076-.793-2.781-.793-5.114 0-2.118.367-3.645 1.102-4.581.732-.934 2.209-1.401 4.43-1.401 1.378 0 2.569.221 3.572.665 1.002.446 1.503 1.497 1.503 3.153v.188a.234.234 0 0 0-.027.106v.106h-1.826c0-1.12-.228-1.851-.685-2.188-.455-.34-1.23-.507-2.322-.507-1.665 0-2.713.259-3.142.782-.43.524-.646 1.67-.646 3.44V12.2c0 1.4.161 2.448.485 3.15.322.7 1.297 1.048 2.926 1.048 1.397 0 2.346-.165 2.848-.495zm15.87-.318c.606-.328.912-1.263.912-2.805 0-.924-.147-1.69-.444-2.302-.294-.611-1.007-.917-2.133-.917H43.065v6.516h5.528c1.543 0 2.617-.165 3.226-.492zm-8.754-13.85v6.036h5.665c1.325 0 2.239-.214 2.737-.643.503-.428.755-1.288.755-2.575 0-1.09-.192-1.834-.577-2.226-.385-.394-1.116-.591-2.19-.591h-6.39zM52 8.466c1.185.322 1.956.816 2.314 1.482.359.67.54 1.581.54 2.739 0 1.6-.325 2.858-.97 3.764-.647.909-1.874 1.363-3.686 1.363H41.05V0h8.62c1.592 0 2.759.312 3.503.934.744.624 1.115 1.78 1.115 3.473 0 1.014-.129 1.855-.386 2.522-.258.669-.89 1.181-1.903 1.536zm5.43 9.348V.001h1.907v17.812H57.43zm13.374-6.168v-.708c0-1.539-.141-2.636-.426-3.297-.284-.658-1.191-.99-2.718-.99h-.401c-1.617 0-2.625.25-3.025.75-.399.5-.599 1.632-.599 3.4V12.822c0 .206.008.407.024.602a9.48 9.48 0 0 0 .216 1.474c.106.465.275.822.505 1.072.196.179.435.302.72.373.283.072.586.116.905.135h1.254c1.083 0 1.914-.151 2.492-.456.577-.302.9-1.043.972-2.22a18.338 18.338 0 0 0 .054-1.447c.015-.233.027-.47.027-.71zm-3.786-6.518c2.399 0 3.956.414 4.672 1.243.717.828 1.074 2.418 1.074 4.766 0 2.048-.219 3.704-.657 4.966C71.668 17.367 70.16 18 67.581 18c-2.237 0-3.782-.347-4.632-1.042-.85-.694-1.275-2.205-1.275-4.54v-1.174c0-2.12.318-3.669.954-4.646.635-.98 2.099-1.471 4.39-1.471zm16.434 10.776c.501-.328.752-1.205.752-2.63h1.853c0 1.924-.403 3.178-1.209 3.765-.804.589-2.157.882-4.054.882-2.648 0-4.24-.538-4.767-1.616-.528-1.076-.79-2.781-.79-5.114 0-2.118.365-3.645 1.1-4.581.732-.934 2.21-1.401 4.43-1.401 1.377 0 2.568.221 3.571.665 1.003.446 1.504 1.497 1.504 3.153v.188a.232.232 0 0 0-.026.106v.106H83.99c0-1.12-.229-1.851-.686-2.188-.455-.34-1.23-.507-2.323-.507-1.665 0-2.712.259-3.142.782-.43.524-.643 1.67-.643 3.44V12.2c0 1.4.16 2.448.482 3.15.324.7 1.298 1.048 2.927 1.048 1.397 0 2.346-.165 2.847-.495zm9.236-4.82L99 17.813h-2.47l-5.317-6.01h-.752v6.01h-1.907V0h1.907v10.362h.752l4.512-5.02h2.309l-5.346 5.741z"/>
3
+ </svg>
@@ -0,0 +1,6 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="45" height="52" viewBox="0 0 45 52">
2
+ <g fill="none" fill-rule="evenodd" stroke="#000">
3
+ <path d="M.5 13.077L22.15.577l21.651 12.5v25l-21.65 12.5L.5 38.077zM22.15.577v50M.5 13.077l43.301 25M.5 38.077l43.301-25"/>
4
+ <path d="M22.15 38.077l10.826-6.25-10.825-18.75-10.825 18.75z"/>
5
+ </g>
6
+ </svg>
@@ -0,0 +1,3 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="99" height="18" viewBox="0 0 99 18">
2
+ <path fill="#FFF" fill-rule="evenodd" d="M6.5.053h2.864L16.028 18h-2.114l-1.391-3.795H3.504L2.14 18H0L6.5.053zm1.474 1.481L4.04 12.563h7.895L7.975 1.534zm14.44 3.542c1.27 0 2.198.296 2.78.892.58.597.871 1.52.871 2.765v.36c0 .117-.008.245-.026.385h-1.826c.018-.14.027-.292.027-.452v-.48c0-.766-.186-1.283-.551-1.55-.367-.266-.916-.4-1.65-.4-.968 0-1.698.24-2.189.723-.493.48-.738 1.208-.738 2.187v8.308h-1.88V5.341h1.824l-.079 1.443c.376-.614.863-1.053 1.463-1.316a4.868 4.868 0 0 1 1.974-.392zM35.95 15.903c.501-.328.752-1.205.752-2.63h1.852c0 1.924-.403 3.178-1.208 3.765-.806.589-2.158.882-4.055.882-2.65 0-4.24-.538-4.766-1.616-.529-1.076-.793-2.781-.793-5.114 0-2.118.367-3.645 1.102-4.581.732-.934 2.209-1.401 4.43-1.401 1.378 0 2.569.221 3.572.665 1.002.446 1.503 1.497 1.503 3.153v.188a.234.234 0 0 0-.027.106v.106h-1.826c0-1.12-.228-1.851-.685-2.188-.455-.34-1.23-.507-2.322-.507-1.665 0-2.713.259-3.142.782-.43.524-.646 1.67-.646 3.44V12.2c0 1.4.161 2.448.485 3.15.322.7 1.297 1.048 2.926 1.048 1.397 0 2.346-.165 2.848-.495zm15.87-.318c.606-.328.912-1.263.912-2.805 0-.924-.147-1.69-.444-2.302-.294-.611-1.007-.917-2.133-.917H43.065v6.516h5.528c1.543 0 2.617-.165 3.226-.492zm-8.754-13.85v6.036h5.665c1.325 0 2.239-.214 2.737-.643.503-.428.755-1.288.755-2.575 0-1.09-.192-1.834-.577-2.226-.385-.394-1.116-.591-2.19-.591h-6.39zM52 8.466c1.185.322 1.956.816 2.314 1.482.359.67.54 1.581.54 2.739 0 1.6-.325 2.858-.97 3.764-.647.909-1.874 1.363-3.686 1.363H41.05V0h8.62c1.592 0 2.759.312 3.503.934.744.624 1.115 1.78 1.115 3.473 0 1.014-.129 1.855-.386 2.522-.258.669-.89 1.181-1.903 1.536zm5.43 9.348V.001h1.907v17.812H57.43zm13.374-6.168v-.708c0-1.539-.141-2.636-.426-3.297-.284-.658-1.191-.99-2.718-.99h-.401c-1.617 0-2.625.25-3.025.75-.399.5-.599 1.632-.599 3.4V12.822c0 .206.008.407.024.602a9.48 9.48 0 0 0 .216 1.474c.106.465.275.822.505 1.072.196.179.435.302.72.373.283.072.586.116.905.135h1.254c1.083 0 1.914-.151 2.492-.456.577-.302.9-1.043.972-2.22a18.338 18.338 0 0 0 .054-1.447c.015-.233.027-.47.027-.71zm-3.786-6.518c2.399 0 3.956.414 4.672 1.243.717.828 1.074 2.418 1.074 4.766 0 2.048-.219 3.704-.657 4.966C71.668 17.367 70.16 18 67.581 18c-2.237 0-3.782-.347-4.632-1.042-.85-.694-1.275-2.205-1.275-4.54v-1.174c0-2.12.318-3.669.954-4.646.635-.98 2.099-1.471 4.39-1.471zm16.434 10.776c.501-.328.752-1.205.752-2.63h1.853c0 1.924-.403 3.178-1.209 3.765-.804.589-2.157.882-4.054.882-2.648 0-4.24-.538-4.767-1.616-.528-1.076-.79-2.781-.79-5.114 0-2.118.365-3.645 1.1-4.581.732-.934 2.21-1.401 4.43-1.401 1.377 0 2.568.221 3.571.665 1.003.446 1.504 1.497 1.504 3.153v.188a.232.232 0 0 0-.026.106v.106H83.99c0-1.12-.229-1.851-.686-2.188-.455-.34-1.23-.507-2.323-.507-1.665 0-2.712.259-3.142.782-.43.524-.643 1.67-.643 3.44V12.2c0 1.4.16 2.448.482 3.15.324.7 1.298 1.048 2.927 1.048 1.397 0 2.346-.165 2.847-.495zm9.236-4.82L99 17.813h-2.47l-5.317-6.01h-.752v6.01h-1.907V0h1.907v10.362h.752l4.512-5.02h2.309l-5.346 5.741z"/>
3
+ </svg>
@@ -0,0 +1,6 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="45" height="52" viewBox="0 0 45 52">
2
+ <g fill="none" fill-rule="evenodd" stroke="#FFF">
3
+ <path d="M.5 13.077L22.15.577l21.651 12.5v25l-21.65 12.5L.5 38.077zM22.15.577v50M.5 13.077l43.301 25M.5 38.077l43.301-25"/>
4
+ <path d="M22.15 38.077l10.826-6.25-10.825-18.75-10.825 18.75z"/>
5
+ </g>
6
+ </svg>
@@ -0,0 +1,47 @@
1
+ import React from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import styled from 'styled-components';
4
+
5
+ import { ReactComponent as LightLogo } from './images/logo-light-top.svg';
6
+ import { ReactComponent as LightText } from './images/logo-light-text.svg';
7
+ import { ReactComponent as DarkLogo } from './images/logo-dark-top.svg';
8
+ import { ReactComponent as DarkText } from './images/logo-dark-text.svg';
9
+
10
+ export default function Logo({ showText, showLogo, mode, layout, ...rest }) {
11
+ const logo = mode === 'light' ? <LightLogo /> : <DarkLogo />;
12
+ const text =
13
+ mode === 'light' ? <LightText className="logo-text" /> : <DarkText className="logo-text" />;
14
+
15
+ return (
16
+ <Container layout={layout} {...rest}>
17
+ {showLogo && logo}
18
+ {showText && text}
19
+ </Container>
20
+ );
21
+ }
22
+
23
+ Logo.propTypes = {
24
+ mode: PropTypes.oneOf(['light', 'dark']),
25
+ layout: PropTypes.oneOf(['vertical', 'horizontal']),
26
+ showText: PropTypes.bool,
27
+ showLogo: PropTypes.bool,
28
+ };
29
+
30
+ Logo.defaultProps = {
31
+ mode: 'dark',
32
+ layout: 'vertical',
33
+ showText: true,
34
+ showLogo: true,
35
+ };
36
+
37
+ const Container = styled.span`
38
+ display: inline-flex;
39
+ flex-direction: ${props => (props.layout === 'horizontal' ? 'row' : 'column')};
40
+ justify-content: center;
41
+ align-items: center;
42
+
43
+ .logo-text {
44
+ ${props => (props.layout === 'vertical' ? 'margin-top: 8px;' : '')}
45
+ ${props => (props.layout === 'vertical' ? '' : 'margin-left: 8px;')};
46
+ }
47
+ `;
@@ -0,0 +1,115 @@
1
+ /* eslint-disable react/no-danger */
2
+ import React from 'react';
3
+ import PropTypes from 'prop-types';
4
+ import styled from 'styled-components';
5
+
6
+ import ImageIcon from '../Icon/image';
7
+
8
+ export default function Metric({ icon, value, name, url, animated, LinkComponent, prefix }) {
9
+ const metric = (
10
+ <>
11
+ <div className="metric__image">
12
+ <ImageIcon name={icon} alt={name} size={30} prefix={prefix} color="#222222" />
13
+ </div>
14
+ <div>
15
+ <div
16
+ className={`metric__number ${animated ? 'metric__number--animated' : ''}`}
17
+ dangerouslySetInnerHTML={{ __html: value }}
18
+ />
19
+ <div className="metric__name">{name}</div>
20
+ </div>
21
+ </>
22
+ );
23
+
24
+ return <Container>{url ? <LinkComponent to={url}>{metric}</LinkComponent> : metric}</Container>;
25
+ }
26
+
27
+ Metric.propTypes = {
28
+ icon: PropTypes.string.isRequired,
29
+ value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
30
+ name: PropTypes.string.isRequired,
31
+ animated: PropTypes.bool,
32
+ url: PropTypes.string,
33
+ LinkComponent: PropTypes.any,
34
+ prefix: PropTypes.string,
35
+ };
36
+
37
+ Metric.defaultProps = {
38
+ animated: false,
39
+ url: '',
40
+ LinkComponent: null,
41
+ prefix: '/images',
42
+ };
43
+
44
+ const Container = styled.div`
45
+ border-left: 1px solid ${props => props.theme.typography.color.main};
46
+ padding: 10px 0 10px 16px;
47
+ @media (max-width: ${props => props.theme.breakpoints.values.sm}px) {
48
+ padding: 0 0 0 8px;
49
+ }
50
+
51
+ display: flex;
52
+ justify-items: center;
53
+ align-items: flex-start;
54
+
55
+ a {
56
+ display: flex;
57
+ justify-items: center;
58
+ align-items: flex-start;
59
+ }
60
+
61
+ .metric__image {
62
+ margin-right: 8px;
63
+ }
64
+
65
+ .metric__number {
66
+ margin-bottom: 8px;
67
+ font-size: ${props => (props.size === 'small' ? 32 : 36)}px;
68
+ font-weight: 600;
69
+ line-height: 36px;
70
+ color: ${props => props.theme.typography.color.main};
71
+
72
+ small {
73
+ font-size: 12px;
74
+ line-height: 12px;
75
+ }
76
+
77
+ @media (max-width: ${props => props.theme.breakpoints.values.sm}px) {
78
+ font-size: ${props => (props.size === 'small' ? 24 : 28)}px;
79
+ line-height: ${props => (props.size === 'small' ? 24 : 28)}px;
80
+ margin-bottom: 2px;
81
+ }
82
+ }
83
+
84
+ .metric__number--animated {
85
+ animation-name: blink-opacity;
86
+ animation-duration: 250ms;
87
+ animation-timing-function: linear;
88
+ animation-iteration-count: 1;
89
+ background-color: transparent !important;
90
+ }
91
+
92
+ .metric__name {
93
+ font-size: 14px;
94
+ text-transform: capitalize;
95
+ line-height: 1.2;
96
+ font-weight: 500;
97
+ color: ${props => props.theme.typography.color.main};
98
+ @media (max-width: ${props => props.theme.breakpoints.values.sm}px) {
99
+ font-size: 10px;
100
+ line-height: 1;
101
+ }
102
+ }
103
+
104
+ @keyframes blink-opacity {
105
+ 0% {
106
+ opacity: 1;
107
+ }
108
+ 50% {
109
+ opacity: 0.3;
110
+ }
111
+ 100% {
112
+ opacity: 1;
113
+ }
114
+ }
115
+ `;
@@ -0,0 +1,59 @@
1
+ # NFTDisplay
2
+
3
+ ## 01 使用
4
+
5
+ ### 01.01 默认样式
6
+
7
+ NFTDisplay 组件默认尺寸为 *150x150*, 可以通过 css 设置 width/height. 固定尺寸的使用场景感觉比较少, 多数情况应该都需要将 NFTDisplay 应用于响应式布局中.
8
+
9
+ ### 01.02 aspect ratio
10
+
11
+ 可以使用 aspect prop 设置 NFTDisplay 的宽高比, NFTDisplay 包裹一两层 div/span 标签 (用来控制布局, 没有直接把 img 标签渲染出来), 所以这里说的长宽比指的不是 img 的长宽比, 图片的尺寸大小不一, 长宽比也是既定的, aspect prop 控制的是包裹 img 容器的长宽比.
12
+
13
+ NFTDisplay 宽高比的计算是根据父容器的 width 来确定的 (NFTDisplay 会填充整个宽度), 所以可以通过控制父容器的 width 来控制 NFTDisplay 的大小.
14
+
15
+ ### 01.03 inset
16
+
17
+ inset 设置为 true 时 NFTDisplay 会镶嵌/填满父容器 (并垂直+水平居中显示图片), 所以父容器必须有明确的宽高 (尤其是高度). 响应式布局中应该会比较常用.
18
+
19
+ 注: inset 比 aspect ratio 优先级高, 如果同时设置了 inset 和 aspect, 则后者不生效.
20
+
21
+ ### 01.04 asset data (prop)
22
+
23
+ 可以为 asset data prop 传入 raw data 或 parsed data, 避免多次解析 asset data.
24
+
25
+ ### 01.05 broken image & error handling
26
+
27
+ * display 加载失败时会显示一个 broken-image, 可以从外部覆盖 `.nft-display--broken` 的样式来调整尺寸/颜色等样式
28
+ * error handling, 这部分感觉出错原因可能性会很多, 有 json 解析错误、某字段缺失、某 object 指定路径的值缺失、nft type 不支持等等, 暂时没想到如何更合理的捕获和处理这些错误.
29
+
30
+ ### 02 svg 加载
31
+
32
+ 基于 `img` 标签加载 NFT Display. 几种 nft type 的加载:
33
+
34
+ - url
35
+ 直接使用 `<img>`
36
+ - svg_gzipped
37
+ content => ungzip => svgToImgUrl => `<img>`
38
+ - svg (未测试, 需要测试数据)
39
+ content => svgToImgUrl => `<img>`
40
+ - html (TODO)
41
+ 基于 `<img>` 的加载方式不适用, 目前没有发现这种 nft type 的测试数据
42
+
43
+ 基于 `<img>` 标签的加载方式没有样式污染问题, 加载、尺寸控制也比较方便, 不过据说有些情况下 svg 可能无法正确显示 (比如 svg 加载了 font|image 资源), 目前没有发现问题, 还需要更多的观察
44
+
45
+ ## 03 iframe 第三方嵌入 (TODO)
46
+
47
+ 需要设计 iframe 的 API (iframe#src 查询参数), 目前想到的参数:
48
+
49
+ - address : asset address
50
+ 调用 getAssetState 接口获取 display 数据进行展示
51
+ - 样式控制相关的参数 ?
52
+
53
+ ## 04 参考
54
+
55
+ * [提供一个 NFT display 组件 · Issue #199 · ArcBlock/ux](https://github.com/ArcBlock/ux/issues/199)
56
+ * [2021徽章出现奇怪的渲染 · Issue #230 · blocklet/nft-marketplace](https://github.com/blocklet/nft-marketplace/issues/230)
57
+ * [Adding vector graphics to the Web - Learn web development | MDN](https://developer.mozilla.org/en-US/docs/Learn/HTML/Multimedia_and_embedding/Adding_vector_graphics_to_the_Web)
58
+ * [The Best Way to Embed SVG on HTML (2021)](https://vecta.io/blog/best-way-to-embed-svg)
59
+ * [javascript - Do I always need to call URL.revokeObjectURL() explicitly? - Stack Overflow](https://stackoverflow.com/questions/49209756/do-i-always-need-to-call-url-revokeobjecturl-explicitly)
@@ -0,0 +1,34 @@
1
+ import React from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import styled from 'styled-components';
4
+
5
+ function AspectRatioContainer({ aspect, children, ...rest }) {
6
+ return (
7
+ <Root aspect={aspect} {...rest}>
8
+ <span className="aspect-ratio-container__inner">{children}</span>
9
+ </Root>
10
+ );
11
+ }
12
+
13
+ AspectRatioContainer.propTypes = {
14
+ aspect: PropTypes.number.isRequired,
15
+ children: PropTypes.node.isRequired,
16
+ };
17
+
18
+ const Root = styled.span`
19
+ display: block;
20
+ position: relative;
21
+ width: 100%;
22
+ height: 0;
23
+ padding-bottom: ${({ aspect }) => (1 / aspect) * 100}%;
24
+
25
+ .aspect-ratio-container__inner {
26
+ position: absolute;
27
+ top: 0;
28
+ bottom: 0;
29
+ left: 0;
30
+ right: 0;
31
+ }
32
+ `;
33
+
34
+ export default AspectRatioContainer;
@@ -0,0 +1,18 @@
1
+ import React from 'react';
2
+ import styled from 'styled-components';
3
+ import BrokenImage from '@material-ui/icons/BrokenImage';
4
+
5
+ export default function Broken() {
6
+ return <Root className="nft-display__broken" />;
7
+ }
8
+
9
+ const Root = styled(BrokenImage)`
10
+ && {
11
+ width: 100%;
12
+ max-width: 200px;
13
+ height: auto;
14
+ max-width: 100%;
15
+ max-height: 100%;
16
+ fill: #ddd;
17
+ }
18
+ `;
@@ -0,0 +1,230 @@
1
+ import React, { useState } from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import styled from 'styled-components';
4
+ import clsx from 'clsx';
5
+ import get from 'lodash/get';
6
+ import pako from 'pako';
7
+ import base64 from 'base64-url';
8
+ import isSvg from 'is-svg';
9
+ import AspectRatioContainer from './aspect-ratio-container';
10
+ import ImgSvgEmbedder from './svg-embedder/img';
11
+ import InlineSvgEmbedder from './svg-embedder/inline-svg';
12
+ import DefaultLoading from './loading';
13
+ import DefaultBrokenImage from './broken';
14
+
15
+ function fromBase64(v) {
16
+ if (typeof v !== 'string') {
17
+ throw new Error('fromBase64 requires input to be a string');
18
+ }
19
+ return Buffer.from(base64.unescape(v), 'base64');
20
+ }
21
+
22
+ const isVC = type => {
23
+ return String(type).includes('VerifiableCredential');
24
+ };
25
+
26
+ // 仅针对非 url type 的情况
27
+ const getSvgEmbedder = preferredSvgEmbedder => {
28
+ const embedders = {
29
+ img: ImgSvgEmbedder,
30
+ svg: InlineSvgEmbedder,
31
+ };
32
+ return embedders[preferredSvgEmbedder];
33
+ };
34
+
35
+ /**
36
+ * TODO:
37
+ * 考虑把 asset data 解析部分和 nft display 分离, android 端有相关使用场景 - 只传入 svg 或 url, 也可以传入 asset data,
38
+ * 目前如果想直接传入 svg 或 url, 需要构造一个 asset data 的数据才能使用 NFTDisplay 组件
39
+ */
40
+ function NFTDisplay({
41
+ data,
42
+ address,
43
+ inset,
44
+ aspect,
45
+ component,
46
+ className,
47
+ renderError,
48
+ renderLoading,
49
+ preferredSvgEmbedder,
50
+ checkSvg,
51
+ minimumLoadingTime,
52
+ onCompleted,
53
+ ...rest
54
+ }) {
55
+ const wrapRoot = children => (
56
+ <Root as={component} {...rest} className={clsx(className, { 'nft-display--inset': inset })}>
57
+ {children}
58
+ </Root>
59
+ );
60
+
61
+ try {
62
+ const parsed = React.useRef(data);
63
+ // 如果是 raw data 先解析
64
+ if (typeof parsed.current === 'string') {
65
+ parsed.current = JSON.parse(data);
66
+ // console.log('[debug] parse data')
67
+ }
68
+ const { vcId, type: assetType } = parsed.current;
69
+ const display = get(parsed.current, 'credentialSubject.display');
70
+ const { content, type } = display;
71
+ const isUrlType = type === 'url';
72
+
73
+ // 首次加载, 对于 url type 的情况, loading 为 true
74
+ const [state, setState] = useState({ loading: isUrlType, error: false });
75
+ const [minimumLoadingReady, setMinimumLoadingReady] = useState(minimumLoadingTime <= 0);
76
+ // console.log('[debug] render', {type, minimumLoadingTime}, JSON.stringify(state))
77
+
78
+ React.useEffect(() => {
79
+ let timer;
80
+ if (minimumLoadingTime > 0) {
81
+ timer = setTimeout(() => setMinimumLoadingReady(true), minimumLoadingTime);
82
+ }
83
+ return () => clearTimeout(timer);
84
+ }, []);
85
+
86
+ // onCompleted
87
+ React.useEffect(() => {
88
+ if ((!state.loading && minimumLoadingReady) || state.error) {
89
+ onCompleted();
90
+ }
91
+ }, [state, minimumLoadingReady]);
92
+
93
+ // VC 检查
94
+ if (state.error || !isVC(assetType)) {
95
+ throw new Error('Failed to render NFT Display.');
96
+ }
97
+
98
+ const renderNFT = () => {
99
+ if (content) {
100
+ switch (type) {
101
+ case 'url': {
102
+ const urlObj = new URL(content);
103
+ urlObj.searchParams.append('assetId', address);
104
+ urlObj.searchParams.append('vcId', vcId);
105
+ const url = urlObj.href;
106
+ return (
107
+ <img
108
+ src={url}
109
+ onError={() => setState({ ...state, error: true })}
110
+ onLoad={() => setState({ ...state, loading: false })}
111
+ alt="NFT Display"
112
+ />
113
+ );
114
+ }
115
+ case 'svg_gzipped': {
116
+ const buffer = pako.ungzip(fromBase64(content), {});
117
+ const svg = Buffer.from(buffer).toString('utf8');
118
+ if (checkSvg && !isSvg(svg)) {
119
+ throw new Error(`Invalid SVG: ${svg}`);
120
+ }
121
+ const Embedder = getSvgEmbedder(preferredSvgEmbedder);
122
+ return <Embedder svg={svg} />;
123
+ }
124
+ case 'svg': {
125
+ if (checkSvg && !isSvg(content)) {
126
+ throw new Error(`Invalid SVG: ${content}`);
127
+ }
128
+ const Embedder = getSvgEmbedder(preferredSvgEmbedder);
129
+ return <Embedder svg={content} />;
130
+ }
131
+ // TODO: 准备测试数据
132
+ case 'html': {
133
+ break;
134
+ }
135
+ default:
136
+ }
137
+ }
138
+ throw new Error(`unsupported display protocol: ${display.type}`);
139
+ };
140
+
141
+ return wrapRoot(
142
+ <>
143
+ {(state.loading || !minimumLoadingReady) &&
144
+ (renderLoading ? renderLoading() : <DefaultLoading />)}
145
+ {renderNFT()}
146
+ </>
147
+ );
148
+ } catch (e) {
149
+ console.error(e);
150
+ return wrapRoot(renderError ? renderError() : <DefaultBrokenImage />);
151
+ }
152
+ }
153
+
154
+ NFTDisplay.propTypes = {
155
+ // asset data 可以是 raw data 和 parsed data
156
+ data: PropTypes.oneOfType([PropTypes.object, PropTypes.string]).isRequired,
157
+ address: PropTypes.string.isRequired,
158
+ component: PropTypes.string,
159
+ inset: PropTypes.bool,
160
+ aspect: PropTypes.number,
161
+ className: PropTypes.string,
162
+ renderError: PropTypes.func,
163
+ renderLoading: PropTypes.func,
164
+ // 对于非 url type 的情况, 支持优先选用的 svg 嵌入方式, 默认是 img
165
+ preferredSvgEmbedder: PropTypes.oneOf(['img', 'svg']),
166
+ // 针对非 url type 的情况, 检测 svg 有效性, 默认禁用
167
+ checkSvg: PropTypes.bool,
168
+ // loading 最小显示时间 (避免闪烁)
169
+ minimumLoadingTime: PropTypes.number,
170
+ // 完成回调, 无论加载成功|失败
171
+ onCompleted: PropTypes.func,
172
+ };
173
+
174
+ NFTDisplay.defaultProps = {
175
+ component: 'span',
176
+ inset: false,
177
+ aspect: 0,
178
+ className: '',
179
+ renderError: null,
180
+ renderLoading: null,
181
+ preferredSvgEmbedder: 'img',
182
+ checkSvg: false,
183
+ minimumLoadingTime: 0,
184
+ onCompleted: () => {},
185
+ };
186
+
187
+ const Root = styled.div`
188
+ display: flex;
189
+ justify-content: center;
190
+ align-items: center;
191
+ position: relative;
192
+ /* 默认尺寸 */
193
+ width: 150px;
194
+ height: 150px;
195
+ overflow: hidden;
196
+
197
+ &,
198
+ img {
199
+ max-width: 100%;
200
+ max-height: 100%;
201
+ }
202
+
203
+ img {
204
+ width: 100%;
205
+ height: 100%;
206
+ }
207
+
208
+ &.nft-display--inset {
209
+ width: 100%;
210
+ height: 100%;
211
+ }
212
+ `;
213
+
214
+ function withAspectRatio(Component) {
215
+ // eslint-disable-next-line func-names, react/prop-types
216
+ return function ({ aspect, inset, ...rest }) {
217
+ // inset 比 aspect ratio 优先级高, 如果同时设置了 inset 和 aspect, 则后者不生效
218
+ const applyAspectRatio = aspect > 0 && !inset;
219
+ if (applyAspectRatio) {
220
+ return (
221
+ <AspectRatioContainer aspect={aspect}>
222
+ <Component inset {...rest} />
223
+ </AspectRatioContainer>
224
+ );
225
+ }
226
+ return <Component inset={inset} {...rest} />;
227
+ };
228
+ }
229
+
230
+ export default withAspectRatio(NFTDisplay);