@apify/ui-library 1.120.0 → 1.121.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 (38) hide show
  1. package/dist/src/components/boring_avatar.d.ts +13 -0
  2. package/dist/src/components/boring_avatar.d.ts.map +1 -0
  3. package/dist/src/components/boring_avatar.js +98 -0
  4. package/dist/src/components/boring_avatar.js.map +1 -0
  5. package/dist/src/components/index.d.ts +2 -0
  6. package/dist/src/components/index.d.ts.map +1 -1
  7. package/dist/src/components/index.js +2 -0
  8. package/dist/src/components/index.js.map +1 -1
  9. package/dist/src/components/readme_renderer/index.d.ts +2 -2
  10. package/dist/src/components/readme_renderer/index.d.ts.map +1 -1
  11. package/dist/src/components/readme_renderer/index.js +1 -1
  12. package/dist/src/components/readme_renderer/index.js.map +1 -1
  13. package/dist/src/components/store/actor_avatar.d.ts +18 -0
  14. package/dist/src/components/store/actor_avatar.d.ts.map +1 -0
  15. package/dist/src/components/store/actor_avatar.js +64 -0
  16. package/dist/src/components/store/actor_avatar.js.map +1 -0
  17. package/dist/src/components/store/index.d.ts +3 -0
  18. package/dist/src/components/store/index.d.ts.map +1 -0
  19. package/dist/src/components/store/index.js +3 -0
  20. package/dist/src/components/store/index.js.map +1 -0
  21. package/dist/src/components/store/store_actor_header.d.ts +13 -0
  22. package/dist/src/components/store/store_actor_header.d.ts.map +1 -0
  23. package/dist/src/components/store/store_actor_header.js +70 -0
  24. package/dist/src/components/store/store_actor_header.js.map +1 -0
  25. package/dist/src/components/tag.d.ts +1 -1
  26. package/dist/src/components/tag.d.ts.map +1 -1
  27. package/dist/tsconfig.build.tsbuildinfo +1 -1
  28. package/package.json +3 -2
  29. package/src/components/boring_avatar.stories.tsx +188 -0
  30. package/src/components/boring_avatar.tsx +262 -0
  31. package/src/components/index.ts +2 -0
  32. package/src/components/readme_renderer/index.ts +2 -2
  33. package/src/components/store/actor_avatar.stories.tsx +65 -0
  34. package/src/components/store/actor_avatar.tsx +129 -0
  35. package/src/components/store/index.ts +2 -0
  36. package/src/components/store/store_actor_header.stories.tsx +126 -0
  37. package/src/components/store/store_actor_header.tsx +131 -0
  38. package/src/components/tag.tsx +1 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@apify/ui-library",
3
- "version": "1.120.0",
3
+ "version": "1.121.0",
4
4
  "description": "React UI library used by apify.com",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -31,6 +31,7 @@
31
31
  "@react-hook/resize-observer": "^2.0.2",
32
32
  "clsx": "^2.0.0",
33
33
  "dayjs": "1.11.9",
34
+ "emoji-regex": "^9.2.2",
34
35
  "fast-average-color": "^9.4.0",
35
36
  "history": "^5.3.0",
36
37
  "lodash": "^4.17.21",
@@ -64,5 +65,5 @@
64
65
  "src",
65
66
  "style"
66
67
  ],
67
- "gitHead": "ad7a5921f83c7575ecea7fca3b3843cdc7f04a8f"
68
+ "gitHead": "234eb45c25c1f6b5f0943da5fc4cd2c5fecd2afd"
68
69
  }
@@ -0,0 +1,188 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite';
2
+ import styled from 'styled-components';
3
+
4
+ import { BoringAvatarMarble, BoringAvatarSolid } from './boring_avatar.js';
5
+
6
+ const meta = {
7
+ title: 'UI-Library/Store/BoringAvatar',
8
+ component: BoringAvatarMarble,
9
+ argTypes: {
10
+ name: {
11
+ control: 'text',
12
+ description: 'Name used to generate the avatar (determines colors and letters)',
13
+ },
14
+ size: {
15
+ control: { type: 'number', min: 16, max: 200 },
16
+ description: 'Size of the avatar in pixels',
17
+ },
18
+ square: {
19
+ control: 'boolean',
20
+ description: 'Whether the avatar should be square instead of circular',
21
+ },
22
+ colors: {
23
+ control: 'object',
24
+ description: 'Array of colors used for the avatar generation',
25
+ },
26
+ alt: {
27
+ control: 'text',
28
+ description: 'Alt text for accessibility',
29
+ },
30
+ className: {
31
+ control: 'text',
32
+ description: 'CSS class name for styling',
33
+ },
34
+ },
35
+ } satisfies Meta<typeof BoringAvatarMarble>;
36
+
37
+ export default meta;
38
+ type Story = StoryObj<typeof meta>;
39
+
40
+ const StoryWrapper = styled.div`
41
+ display: flex;
42
+ flex-wrap: wrap;
43
+ gap: 1rem;
44
+ padding: 2rem;
45
+ align-items: center;
46
+ `;
47
+
48
+ const AvatarShowcase = styled.div`
49
+ display: flex;
50
+ flex-direction: column;
51
+ align-items: center;
52
+ gap: 0.5rem;
53
+
54
+ small {
55
+ color: #666;
56
+ font-size: 0.75rem;
57
+ }
58
+ `;
59
+
60
+ const GridWrapper = styled.div`
61
+ display: grid;
62
+ grid-template-columns: repeat(auto-fill, minmax(80px, 1fr));
63
+ gap: 1.5rem;
64
+ padding: 2rem;
65
+ `;
66
+
67
+ /**
68
+ * Default marble avatar with standard settings
69
+ */
70
+ export const MarbleDefault: Story = {
71
+ args: {
72
+ name: 'John Doe',
73
+ size: 64,
74
+ },
75
+ };
76
+
77
+ /**
78
+ * Default solid color avatar
79
+ */
80
+ export const SolidDefault: Story = {
81
+ render: (args) => <BoringAvatarSolid {...args} />,
82
+ args: {
83
+ name: 'John Doe',
84
+ size: 64,
85
+ },
86
+ };
87
+
88
+ /**
89
+ * Different sizes comparison
90
+ */
91
+ export const Sizes: Story = {
92
+ render: () => (
93
+ <StoryWrapper>
94
+ <AvatarShowcase>
95
+ <BoringAvatarMarble name="Alice" size={24} />
96
+ <small>24px</small>
97
+ </AvatarShowcase>
98
+ <AvatarShowcase>
99
+ <BoringAvatarMarble name="Alice" size={32} />
100
+ <small>32px</small>
101
+ </AvatarShowcase>
102
+ <AvatarShowcase>
103
+ <BoringAvatarMarble name="Alice" size={48} />
104
+ <small>48px</small>
105
+ </AvatarShowcase>
106
+ <AvatarShowcase>
107
+ <BoringAvatarMarble name="Alice" size={64} />
108
+ <small>64px</small>
109
+ </AvatarShowcase>
110
+ <AvatarShowcase>
111
+ <BoringAvatarMarble name="Alice" size={96} />
112
+ <small>96px</small>
113
+ </AvatarShowcase>
114
+ <AvatarShowcase>
115
+ <BoringAvatarMarble name="Alice" size={128} />
116
+ <small>128px</small>
117
+ </AvatarShowcase>
118
+ </StoryWrapper>
119
+ ),
120
+ };
121
+
122
+ /**
123
+ * Circular vs Square shapes
124
+ */
125
+ export const Shapes: Story = {
126
+ render: () => (
127
+ <StoryWrapper>
128
+ <AvatarShowcase>
129
+ <BoringAvatarMarble name="Shape Test" size={80} square={false} />
130
+ <small>Circular</small>
131
+ </AvatarShowcase>
132
+ <AvatarShowcase>
133
+ <BoringAvatarMarble name="Shape Test" size={80} square />
134
+ <small>Square (broken?)</small>
135
+ </AvatarShowcase>
136
+ <AvatarShowcase>
137
+ <BoringAvatarSolid name="Shape Test" size={80} square={false} />
138
+ <small>Circular (Solid)</small>
139
+ </AvatarShowcase>
140
+ <AvatarShowcase>
141
+ <BoringAvatarSolid name="Shape Test" size={80} square />
142
+ <small>Square (Solid) (broken?)</small>
143
+ </AvatarShowcase>
144
+ </StoryWrapper>
145
+ ),
146
+ };
147
+
148
+ /**
149
+ * Different names generate different avatars
150
+ */
151
+ export const DifferentNames: Story = {
152
+ render: () => (
153
+ <GridWrapper>
154
+ <AvatarShowcase>
155
+ <BoringAvatarMarble name="John Doe" size={64} />
156
+ <small>John Doe</small>
157
+ </AvatarShowcase>
158
+ <AvatarShowcase>
159
+ <BoringAvatarMarble name="Jane Smith" size={64} />
160
+ <small>Jane Smith</small>
161
+ </AvatarShowcase>
162
+ <AvatarShowcase>
163
+ <BoringAvatarMarble name="web-scraper" size={64} />
164
+ <small>web-scraper</small>
165
+ </AvatarShowcase>
166
+ <AvatarShowcase>
167
+ <BoringAvatarMarble name="my-actor" size={64} />
168
+ <small>my-actor</small>
169
+ </AvatarShowcase>
170
+ <AvatarShowcase>
171
+ <BoringAvatarMarble name="apify/store" size={64} />
172
+ <small>apify/store</small>
173
+ </AvatarShowcase>
174
+ <AvatarShowcase>
175
+ <BoringAvatarMarble name="data.processor" size={64} />
176
+ <small>data.processor</small>
177
+ </AvatarShowcase>
178
+ <AvatarShowcase>
179
+ <BoringAvatarMarble name="APIIntegration" size={64} />
180
+ <small>APIIntegration</small>
181
+ </AvatarShowcase>
182
+ <AvatarShowcase>
183
+ <BoringAvatarMarble name="test_user_123" size={64} />
184
+ <small>test_user_123</small>
185
+ </AvatarShowcase>
186
+ </GridWrapper>
187
+ ),
188
+ };
@@ -0,0 +1,262 @@
1
+ import emojiRegex from 'emoji-regex';
2
+ import React, { useMemo } from 'react';
3
+
4
+ const emojiRegexPattern = emojiRegex();
5
+
6
+ // This code is taken from https://github.com/boringdesigners/boring-avatars/blob/master/src/lib/components/avatar-marble.js
7
+
8
+ const ELEMENTS = 3;
9
+ const SIZE = 100;
10
+
11
+ const IGNORED_WORDS = ['AND', 'A', 'THE', 'AN'];
12
+
13
+ function hashCode(name?: string) {
14
+ // This is very defensive, but passing empty string as name to this component by mistake
15
+ // should never break the UI completely.
16
+ if (!name) return 0;
17
+
18
+ let hash = 0;
19
+ for (let i = 0; i < name.length; i++) {
20
+ const character = name.charCodeAt(i);
21
+ hash = (hash << 5) - hash + character; // eslint-disable-line
22
+ hash &= hash; // eslint-disable-line
23
+ }
24
+ return Math.abs(hash);
25
+ }
26
+
27
+ function getDigit(number: number, ntn: number) {
28
+ return Math.floor((number / 10 ** ntn) % 10);
29
+ }
30
+
31
+ function getUnit(number: number, range: number, index?: number) {
32
+ const value = number % range;
33
+
34
+ if (index && getDigit(number, index) % 2 === 0) {
35
+ return -value;
36
+ }
37
+ return value;
38
+ }
39
+
40
+ function generateProperties(name: string, colors: string[]) {
41
+ const numFromName = hashCode(name);
42
+
43
+ const elementsProperties = [];
44
+ for (let i = 0; i < ELEMENTS; i++) {
45
+ elementsProperties.push({
46
+ color: colors[(numFromName + (i + 1)) % colors.length],
47
+ translateX: getUnit(numFromName * (i + 1), SIZE / 10, 1),
48
+ translateY: getUnit(numFromName * (i + 1), SIZE / 10, 2),
49
+ scale: 1.2 + getUnit(numFromName * (i + 1), SIZE / 20) / 10,
50
+ rotate: getUnit(numFromName * (i + 1), 360, 1),
51
+ });
52
+ }
53
+
54
+ return elementsProperties;
55
+ }
56
+
57
+ function generateFill(
58
+ properties: ReturnType<typeof generateProperties>,
59
+ name: string,
60
+ ) {
61
+ const {
62
+ translateX: translateX1,
63
+ translateY: translateY1,
64
+ rotate: rotate1,
65
+ } = properties[1];
66
+ const {
67
+ translateX: translateX2,
68
+ translateY: translateY2,
69
+ rotate: rotate2,
70
+ scale,
71
+ } = properties[2];
72
+
73
+ let nameParts = name
74
+ .replace(emojiRegexPattern, ' ') // Remove emojis from the name
75
+ .trim().replace(/\s+/g, ' ') // Remove multiple spaces
76
+ .toUpperCase()
77
+ .split(' '); // Ideally if the Actor has a name with spaces, we take words separated by spaces
78
+
79
+ if (nameParts.length <= 1) nameParts = nameParts[0].split('-'); // If that did not work, we try to separate it by -
80
+ if (nameParts.length <= 1) nameParts = nameParts[0].split('_'); // If those are also not present we try to separate it by _
81
+ if (nameParts.length <= 1) nameParts = nameParts[0].split('.'); // If those are also not present we try to separate it by .
82
+ if (nameParts.length <= 1) nameParts = nameParts[0].split(''); // In the end we take the first word and separate it by letter
83
+
84
+ // Nice to have: remove ignored words from the name if the name has more than 2 words
85
+ if (nameParts.length > 2) { nameParts = nameParts.filter((part) => !IGNORED_WORDS.includes(part)); }
86
+
87
+ return {
88
+ fill0: properties[0].color,
89
+ fill1: properties[1].color,
90
+ fill2: properties[2].color,
91
+ transform1: `translate(${translateX1} ${translateY1}) rotate(${rotate1} ${SIZE / 2} ${SIZE / 2}) scale(${scale})`,
92
+ transform2: `translate(${translateX2} ${translateY2}) rotate(${rotate2} ${SIZE / 2} ${SIZE / 2}) scale(${scale})`,
93
+ letter1: nameParts?.[0]?.[0] ?? undefined,
94
+ letter2: nameParts?.[1]?.[0] ?? undefined,
95
+ };
96
+ }
97
+
98
+ interface BoringAvatarProps {
99
+ name?: string;
100
+ className?: string;
101
+ size: number;
102
+ colors?: string[];
103
+ square?: boolean;
104
+ alt?: string;
105
+ }
106
+
107
+ export const BoringAvatarMarble = React.memo(({
108
+ name = '',
109
+ colors = ['#12966F', '#5D85E1', '#E44467', '#F0B21B', '#FA8136', '#30C0BB'],
110
+ square = false,
111
+ className,
112
+ size = 24,
113
+ alt = 'Avatar image',
114
+ }: BoringAvatarProps) => {
115
+ const fill = useMemo(() => {
116
+ const properties = generateProperties(name ?? '', colors);
117
+ return generateFill(properties, name ?? '');
118
+ }, [name, colors]);
119
+
120
+ return (
121
+ <svg
122
+ viewBox={`0 0 ${SIZE} ${SIZE}`}
123
+ fill="none"
124
+ role="img"
125
+ xmlns="http://www.w3.org/2000/svg"
126
+ width={size}
127
+ height={size}
128
+ className={className}
129
+ aria-label={alt}
130
+ >
131
+ <mask
132
+ id="mask__marble"
133
+ maskUnits="userSpaceOnUse"
134
+ x={0}
135
+ y={0}
136
+ width={SIZE}
137
+ height={SIZE}
138
+ >
139
+ <rect
140
+ width={SIZE}
141
+ height={SIZE}
142
+ rx={square ? undefined : SIZE * 2}
143
+ fill="#FFFFFF"
144
+ />
145
+ </mask>
146
+ <g mask="url(#mask__marble)">
147
+ <rect width={SIZE} height={SIZE} fill={fill.fill0} />
148
+ <path
149
+ filter="url(#prefix__filter0_f)"
150
+ d="M32.414 59.35L50.376 70.5H72.5v-71H33.728L26.5 13.381l19.057 27.08L32.414 59.35z"
151
+ fill={fill.fill1}
152
+ transform={fill.transform1}
153
+ />
154
+ <path
155
+ filter="url(#prefix__filter0_f)"
156
+ style={{
157
+ mixBlendMode: 'overlay',
158
+ }}
159
+ d="M22.216 24L0 46.75l14.108 38.129L78 86l-3.081-59.276-22.378 4.005 12.972 20.186-23.35 27.395L22.215 24z"
160
+ fill={fill.fill2}
161
+ transform={fill.transform2}
162
+ />
163
+ </g>
164
+ <g>
165
+ {fill.letter1 && (
166
+ <text
167
+ style={{ fontSize: '50px' }}
168
+ x="50"
169
+ y="50"
170
+ dy="7"
171
+ textAnchor="middle"
172
+ alignmentBaseline="middle"
173
+ dominantBaseline="middle"
174
+ >
175
+ {fill.letter1}
176
+ {fill.letter2}
177
+ </text>
178
+ )}
179
+ </g>
180
+ <defs>
181
+ <filter
182
+ id="prefix__filter0_f"
183
+ filterUnits="userSpaceOnUse"
184
+ colorInterpolationFilters="sRGB"
185
+ >
186
+ <feFlood floodOpacity={0} result="BackgroundImageFix" />
187
+ <feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape" />
188
+ <feGaussianBlur stdDeviation={7} result="effect1_foregroundBlur" />
189
+ </filter>
190
+ </defs>
191
+ </svg>
192
+ );
193
+ });
194
+
195
+ export const BoringAvatarSolid = React.memo(({
196
+ name = '',
197
+ alt = 'Avatar image',
198
+ colors = ['#B8487B', '#E5B557', '#58BCC4', '#6D85CA', '#8690BF', '#F08848', '#8148C9', '#439274'],
199
+ square = false,
200
+ className,
201
+ size = 24,
202
+ }: BoringAvatarProps) => {
203
+ /**
204
+ * Name must be lowercased to avoid irregularities caused by caps.
205
+ * This is an issue in the Actor detail header, where the username comes from the Actor object,
206
+ * while in the user selector it comes from the user object, where it can have capitals.
207
+ */
208
+ const nameLowercase = name.toLowerCase();
209
+
210
+ const fill = useMemo(() => {
211
+ const properties = generateProperties(nameLowercase, colors);
212
+ return generateFill(properties, nameLowercase);
213
+ }, [nameLowercase, colors]);
214
+
215
+ return (
216
+ <svg
217
+ viewBox={`0 0 ${SIZE} ${SIZE}`}
218
+ fill="none"
219
+ role="img"
220
+ xmlns="http://www.w3.org/2000/svg"
221
+ width={size}
222
+ height={size}
223
+ className={className}
224
+ aria-label={alt}
225
+ >
226
+ <mask
227
+ id="mask__solid"
228
+ maskUnits="userSpaceOnUse"
229
+ x={0}
230
+ y={0}
231
+ width={SIZE}
232
+ height={SIZE}
233
+ >
234
+ <rect
235
+ width={SIZE}
236
+ height={SIZE}
237
+ rx={square ? undefined : SIZE * 2}
238
+ fill="#FFFFFF"
239
+ />
240
+ </mask>
241
+ <g mask="url(#mask__solid)">
242
+ <rect width={SIZE} height={SIZE} fill={fill.fill0} />
243
+ </g>
244
+ <g>
245
+ {fill.letter1 && (
246
+ <text
247
+ style={{ fontSize: '50px' }}
248
+ x="50"
249
+ y="50"
250
+ dy="7"
251
+ textAnchor="middle"
252
+ alignmentBaseline="middle"
253
+ dominantBaseline="middle"
254
+ >
255
+ {fill.letter1}
256
+ {fill.letter2}
257
+ </text>
258
+ )}
259
+ </g>
260
+ </svg>
261
+ );
262
+ });
@@ -1,5 +1,6 @@
1
1
  export * from './text/index.js';
2
2
  export * from './box.js';
3
+ export * from './boring_avatar.js';
3
4
  export * from './card_container.js';
4
5
  export * from './message.js';
5
6
  export * from './floating/index.js';
@@ -24,3 +25,4 @@ export * from './tabs/index.js';
24
25
  export * from './shortcut.js';
25
26
  export * from './icon_button.js';
26
27
  export * from './spinner.js';
28
+ export * from './store/index.js';
@@ -1,3 +1,3 @@
1
- export { useActorTitleHeadingFilter } from './utils.js';
1
+ export { useActorTitleHeadingFilter, cleanMarkdown, slugifyHeadingChildren } from './utils.js';
2
2
  export { pythonizeValue } from './pythonize_value.js';
3
- export { TableOfContents, useShowTableOfContents } from './table_of_contents.js';
3
+ export { TableOfContents, type TableOfContentsProps, useShowTableOfContents } from './table_of_contents.js';
@@ -0,0 +1,65 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite';
2
+ import React from 'react';
3
+
4
+ import { ActorAvatar } from './actor_avatar.js';
5
+
6
+ const meta = {
7
+ title: 'UI-Library/Store/ActorAvatar',
8
+ component: ActorAvatar,
9
+ parameters: {
10
+ layout: 'padded',
11
+ },
12
+ argTypes: {
13
+ name: {
14
+ control: 'text',
15
+ description: 'Name used to generate the avatar when no URL is provided',
16
+ },
17
+ url: {
18
+ control: 'text',
19
+ description: 'URL of the avatar image',
20
+ },
21
+ size: {
22
+ control: 'number',
23
+ description: 'Size of the avatar in pixels',
24
+ },
25
+ alt: {
26
+ control: 'text',
27
+ description: 'Alt text for the avatar image',
28
+ },
29
+ },
30
+ } satisfies Meta<typeof ActorAvatar>;
31
+
32
+ export default meta;
33
+ type Story = StoryObj<typeof meta>;
34
+
35
+ export const Default: Story = {
36
+ args: {
37
+ name: 'Web Scraper',
38
+ size: 64,
39
+ },
40
+ };
41
+
42
+ export const Sizes: Story = {
43
+ render: () => (
44
+ <div style={{ display: 'flex', gap: '16px', alignItems: 'flex-end' }}>
45
+ {[24, 32, 40, 48, 64, 80, 128].map((size) => (
46
+ <div key={size} style={{ textAlign: 'center' }}>
47
+ <ActorAvatar name="Web Scraper" size={size} />
48
+ <p style={{ marginTop: '8px', fontSize: '12px' }}>{size}px</p>
49
+ </div>
50
+ ))}
51
+ </div>
52
+ ),
53
+ };
54
+
55
+ export const WithImageUrl: Story = {
56
+ render: () => (
57
+ <div style={{ display: 'flex', gap: '16px', flexWrap: 'wrap', marginTop: '16px' }}>
58
+ <ActorAvatar
59
+ name="Web Scraper"
60
+ url="https://apify.github.io/apify-web/img/apify-logo/logomark-32x32.svg"
61
+ size={64}
62
+ />
63
+ </div>
64
+ ),
65
+ };
@@ -0,0 +1,129 @@
1
+ import clsx from 'clsx';
2
+ import type { FC, RefCallback } from 'react';
3
+ import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
4
+ import styled from 'styled-components';
5
+
6
+ import {
7
+ BoringAvatarMarble,
8
+ theme,
9
+ useImageColor,
10
+ useSharedUiDependencies,
11
+ } from '../../index.js';
12
+
13
+ export const actorAvatarClassnames = {
14
+ BASE: 'ActorAvatar',
15
+ IMG: 'ActorAvatar-image',
16
+ SVG: 'ActorAvatar-svg',
17
+ };
18
+
19
+ const StyledBoringAvatarMarble = styled(BoringAvatarMarble)`
20
+ border-radius: ${theme.radius.radius8};
21
+
22
+ text {
23
+ ${theme.typography.shared.mobile.bodyMStrong}
24
+
25
+ @media (min-width: ${theme.layout.tablet}) {
26
+ ${theme.typography.shared.tablet.bodyMStrong}
27
+ }
28
+ @media (min-width: ${theme.layout.desktop}) {
29
+ ${theme.typography.shared.desktop.bodyMStrong}
30
+ }
31
+
32
+ fill: ${theme.color.neutral.background};
33
+ color: ${theme.color.neutral.background};
34
+ text-transform: uppercase;
35
+ white-space: pre;
36
+ }
37
+ `;
38
+
39
+ const StyledImg = styled.img`
40
+ border-radius: ${theme.radius.radius8};
41
+ `;
42
+
43
+ interface ActorAvatarProps extends React.ImgHTMLAttributes<HTMLImageElement> {
44
+ name?: string;
45
+ className?: string;
46
+ url?: string;
47
+ alt?: string;
48
+ size: number;
49
+ }
50
+
51
+ export const ActorAvatarInner: FC<ActorAvatarProps> = ({
52
+ name,
53
+ className,
54
+ url,
55
+ size,
56
+ alt,
57
+ ...rest
58
+ }) => {
59
+ const [hasImageError, setHasImageError] = useState(false);
60
+ useEffect(() => {
61
+ setHasImageError(false);
62
+ }, [url]);
63
+
64
+ const { generateProxyImageUrl } = useSharedUiDependencies();
65
+
66
+ const maybeProxiedUrl = useMemo(
67
+ () => (
68
+ generateProxyImageUrl && url
69
+ ? generateProxyImageUrl(url, { resize: { height: size * 2, width: size * 2 } })
70
+ : url
71
+ ),
72
+ [generateProxyImageUrl, url, size],
73
+ );
74
+
75
+ const { color, refCallback } = useImageColor();
76
+
77
+ const imageRef = useRef<HTMLImageElement | null>(null);
78
+ const imageRefCallback = useCallback<RefCallback<HTMLImageElement>>(
79
+ (element) => {
80
+ imageRef.current = element;
81
+ refCallback(element);
82
+ },
83
+ [refCallback],
84
+ );
85
+
86
+ const onError = useCallback<React.ReactEventHandler<HTMLImageElement>>(() => setHasImageError(true), []);
87
+
88
+ useLayoutEffect(() => {
89
+ if (imageRef.current) {
90
+ imageRef.current.style.backgroundColor = color?.isLight
91
+ ? '#1a1b21' // TODO: introduce a static theme color variable (e.g.: theme.color.neutral.backgroundDark)
92
+ : theme.color.neutral.backgroundWhite;
93
+ }
94
+ }, [color?.isLight]);
95
+
96
+ const fullClassName = clsx(
97
+ actorAvatarClassnames.BASE,
98
+ maybeProxiedUrl ? actorAvatarClassnames.IMG : actorAvatarClassnames.SVG,
99
+ className,
100
+ );
101
+
102
+ const altText = alt || `${name} avatar`;
103
+ if (maybeProxiedUrl && !hasImageError) {
104
+ return (
105
+ <StyledImg
106
+ ref={imageRefCallback}
107
+ crossOrigin='anonymous'
108
+ src={maybeProxiedUrl}
109
+ className={fullClassName}
110
+ width={size}
111
+ height={size}
112
+ alt={altText}
113
+ onError={onError}
114
+ {...rest}
115
+ />
116
+ );
117
+ }
118
+ return (
119
+ <StyledBoringAvatarMarble
120
+ size={size}
121
+ className={fullClassName}
122
+ name={name}
123
+ square={true}
124
+ alt={altText}
125
+ />
126
+ );
127
+ };
128
+
129
+ export const ActorAvatar = React.memo(ActorAvatarInner);
@@ -0,0 +1,2 @@
1
+ export * from './store_actor_header.js';
2
+ export * from './actor_avatar.js';