@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.
- package/dist/src/components/boring_avatar.d.ts +13 -0
- package/dist/src/components/boring_avatar.d.ts.map +1 -0
- package/dist/src/components/boring_avatar.js +98 -0
- package/dist/src/components/boring_avatar.js.map +1 -0
- package/dist/src/components/index.d.ts +2 -0
- package/dist/src/components/index.d.ts.map +1 -1
- package/dist/src/components/index.js +2 -0
- package/dist/src/components/index.js.map +1 -1
- package/dist/src/components/readme_renderer/index.d.ts +2 -2
- package/dist/src/components/readme_renderer/index.d.ts.map +1 -1
- package/dist/src/components/readme_renderer/index.js +1 -1
- package/dist/src/components/readme_renderer/index.js.map +1 -1
- package/dist/src/components/store/actor_avatar.d.ts +18 -0
- package/dist/src/components/store/actor_avatar.d.ts.map +1 -0
- package/dist/src/components/store/actor_avatar.js +64 -0
- package/dist/src/components/store/actor_avatar.js.map +1 -0
- package/dist/src/components/store/index.d.ts +3 -0
- package/dist/src/components/store/index.d.ts.map +1 -0
- package/dist/src/components/store/index.js +3 -0
- package/dist/src/components/store/index.js.map +1 -0
- package/dist/src/components/store/store_actor_header.d.ts +13 -0
- package/dist/src/components/store/store_actor_header.d.ts.map +1 -0
- package/dist/src/components/store/store_actor_header.js +70 -0
- package/dist/src/components/store/store_actor_header.js.map +1 -0
- package/dist/src/components/tag.d.ts +1 -1
- package/dist/src/components/tag.d.ts.map +1 -1
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +3 -2
- package/src/components/boring_avatar.stories.tsx +188 -0
- package/src/components/boring_avatar.tsx +262 -0
- package/src/components/index.ts +2 -0
- package/src/components/readme_renderer/index.ts +2 -2
- package/src/components/store/actor_avatar.stories.tsx +65 -0
- package/src/components/store/actor_avatar.tsx +129 -0
- package/src/components/store/index.ts +2 -0
- package/src/components/store/store_actor_header.stories.tsx +126 -0
- package/src/components/store/store_actor_header.tsx +131 -0
- 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.
|
|
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": "
|
|
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
|
+
});
|
package/src/components/index.ts
CHANGED
|
@@ -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);
|