@apify/ui-library 1.168.3 → 1.170.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 (105) hide show
  1. package/dist/components/index.d.ts +1 -0
  2. package/dist/components/index.d.ts.map +1 -1
  3. package/dist/components/skeleton/index.d.ts +6 -0
  4. package/dist/components/skeleton/index.d.ts.map +1 -0
  5. package/dist/components/skeleton/skeleton.d.ts +18 -0
  6. package/dist/components/skeleton/skeleton.d.ts.map +1 -0
  7. package/dist/components/skeleton/skeleton.js +19 -0
  8. package/dist/components/skeleton/skeleton.styled.d.ts +13 -0
  9. package/dist/components/skeleton/skeleton.styled.d.ts.map +1 -0
  10. package/dist/components/skeleton/skeleton.styled.js +55 -0
  11. package/dist/components/skeleton/skeleton.test_ids.d.ts +6 -0
  12. package/dist/components/skeleton/skeleton.test_ids.d.ts.map +1 -0
  13. package/dist/components/skeleton/skeleton.test_ids.js +8 -0
  14. package/dist/components/skeleton/skeleton.type-test.d.ts +2 -0
  15. package/dist/components/skeleton/skeleton.type-test.d.ts.map +1 -0
  16. package/dist/components/skeleton/skeleton_fade_in.d.ts +9 -0
  17. package/dist/components/skeleton/skeleton_fade_in.d.ts.map +1 -0
  18. package/dist/components/skeleton/skeleton_fade_in.js +15 -0
  19. package/dist/components/skeleton/skeleton_group.d.ts +13 -0
  20. package/dist/components/skeleton/skeleton_group.d.ts.map +1 -0
  21. package/dist/components/skeleton/skeleton_group.js +19 -0
  22. package/dist/design_system/colors/generated/colors_theme.dark.d.ts +13 -0
  23. package/dist/design_system/colors/generated/colors_theme.dark.d.ts.map +1 -1
  24. package/dist/design_system/colors/generated/colors_theme.dark.js +13 -0
  25. package/dist/design_system/colors/generated/colors_theme.light.d.ts +13 -0
  26. package/dist/design_system/colors/generated/colors_theme.light.d.ts.map +1 -1
  27. package/dist/design_system/colors/generated/colors_theme.light.js +13 -0
  28. package/dist/design_system/theme.d.ts +26 -0
  29. package/dist/design_system/theme.d.ts.map +1 -1
  30. package/dist/files_hash_at_build.txt +1 -1
  31. package/dist/index.js +6 -1
  32. package/dist/src/components/index.d.ts +1 -0
  33. package/dist/src/components/index.d.ts.map +1 -1
  34. package/dist/src/components/index.js +1 -0
  35. package/dist/src/components/index.js.map +1 -1
  36. package/dist/src/components/skeleton/index.d.ts +6 -0
  37. package/dist/src/components/skeleton/index.d.ts.map +1 -0
  38. package/dist/src/components/skeleton/index.js +6 -0
  39. package/dist/src/components/skeleton/index.js.map +1 -0
  40. package/dist/src/components/skeleton/skeleton.d.ts +18 -0
  41. package/dist/src/components/skeleton/skeleton.d.ts.map +1 -0
  42. package/dist/src/components/skeleton/skeleton.js +10 -0
  43. package/dist/src/components/skeleton/skeleton.js.map +1 -0
  44. package/dist/src/components/skeleton/skeleton.styled.d.ts +13 -0
  45. package/dist/src/components/skeleton/skeleton.styled.d.ts.map +1 -0
  46. package/dist/src/components/skeleton/skeleton.styled.js +55 -0
  47. package/dist/src/components/skeleton/skeleton.styled.js.map +1 -0
  48. package/dist/src/components/skeleton/skeleton.test_ids.d.ts +6 -0
  49. package/dist/src/components/skeleton/skeleton.test_ids.d.ts.map +1 -0
  50. package/dist/src/components/skeleton/skeleton.test_ids.js +6 -0
  51. package/dist/src/components/skeleton/skeleton.test_ids.js.map +1 -0
  52. package/dist/src/components/skeleton/skeleton.type-test.d.ts +2 -0
  53. package/dist/src/components/skeleton/skeleton.type-test.d.ts.map +1 -0
  54. package/dist/src/components/skeleton/skeleton.type-test.js +25 -0
  55. package/dist/src/components/skeleton/skeleton.type-test.js.map +1 -0
  56. package/dist/src/components/skeleton/skeleton_fade_in.d.ts +9 -0
  57. package/dist/src/components/skeleton/skeleton_fade_in.d.ts.map +1 -0
  58. package/dist/src/components/skeleton/skeleton_fade_in.js +9 -0
  59. package/dist/src/components/skeleton/skeleton_fade_in.js.map +1 -0
  60. package/dist/src/components/skeleton/skeleton_group.d.ts +13 -0
  61. package/dist/src/components/skeleton/skeleton_group.d.ts.map +1 -0
  62. package/dist/src/components/skeleton/skeleton_group.js +10 -0
  63. package/dist/src/components/skeleton/skeleton_group.js.map +1 -0
  64. package/dist/src/design_system/colors/generated/colors_theme.dark.d.ts +13 -0
  65. package/dist/src/design_system/colors/generated/colors_theme.dark.d.ts.map +1 -1
  66. package/dist/src/design_system/colors/generated/colors_theme.dark.js +13 -0
  67. package/dist/src/design_system/colors/generated/colors_theme.dark.js.map +1 -1
  68. package/dist/src/design_system/colors/generated/colors_theme.light.d.ts +13 -0
  69. package/dist/src/design_system/colors/generated/colors_theme.light.d.ts.map +1 -1
  70. package/dist/src/design_system/colors/generated/colors_theme.light.js +13 -0
  71. package/dist/src/design_system/colors/generated/colors_theme.light.js.map +1 -1
  72. package/dist/src/design_system/theme.d.ts +26 -0
  73. package/dist/src/design_system/theme.d.ts.map +1 -1
  74. package/dist/src/test_ids.d.ts +1 -0
  75. package/dist/src/test_ids.d.ts.map +1 -1
  76. package/dist/src/test_ids.js +1 -0
  77. package/dist/src/test_ids.js.map +1 -1
  78. package/dist/test_ids.d.ts +1 -0
  79. package/dist/test_ids.d.ts.map +1 -1
  80. package/dist/test_ids.js +2 -1
  81. package/dist/tsconfig.build.tsbuildinfo +1 -1
  82. package/package.json +3 -3
  83. package/src/components/box/box.stories.tsx +1 -0
  84. package/src/components/color_wheel_gradient/color_wheel_gradient.stories.tsx +1 -6
  85. package/src/components/image/image.stories.tsx +1 -0
  86. package/src/components/index.ts +1 -0
  87. package/src/components/readme_renderer/table_of_contents.stories.tsx +1 -0
  88. package/src/components/skeleton/examples/actor_idea_cards_skeleton.example.tsx +67 -0
  89. package/src/components/skeleton/examples/new_actor_wizard_cards_skeleton.example.tsx +55 -0
  90. package/src/components/skeleton/examples/publication_categories_skeleton.example.tsx +50 -0
  91. package/src/components/skeleton/examples/store_actor_cards_loading.example.tsx +74 -0
  92. package/src/components/skeleton/examples/store_actor_cards_skeleton.example.tsx +94 -0
  93. package/src/components/skeleton/index.ts +5 -0
  94. package/src/components/skeleton/skeleton.stories.tsx +221 -0
  95. package/src/components/skeleton/skeleton.styled.ts +66 -0
  96. package/src/components/skeleton/skeleton.test_ids.ts +5 -0
  97. package/src/components/skeleton/skeleton.tsx +34 -0
  98. package/src/components/skeleton/skeleton.type-test.ts +32 -0
  99. package/src/components/skeleton/skeleton_fade_in.tsx +21 -0
  100. package/src/components/skeleton/skeleton_group.tsx +32 -0
  101. package/src/design_system/colors/build_color_tokens.js +4 -2
  102. package/src/design_system/colors/colors.stories.tsx +9 -1
  103. package/src/design_system/colors/generated/colors_theme.dark.ts +13 -0
  104. package/src/design_system/colors/generated/colors_theme.light.ts +13 -0
  105. package/src/test_ids.ts +1 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@apify/ui-library",
3
- "version": "1.168.3",
3
+ "version": "1.170.0",
4
4
  "description": "React UI library used by apify.com",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -42,7 +42,7 @@
42
42
  "It's not nice, but helps us to get around the problem of multiple react instances."
43
43
  ],
44
44
  "dependencies": {
45
- "@apify/ui-icons": "^1.54.0",
45
+ "@apify/ui-icons": "^1.55.0",
46
46
  "@floating-ui/react": "^0.27.19",
47
47
  "@floating-ui/react-dom": "^2.1.8",
48
48
  "@radix-ui/react-checkbox": "^1.3.3",
@@ -94,5 +94,5 @@
94
94
  "src",
95
95
  "style"
96
96
  ],
97
- "gitHead": "fed97f595c48ce93b0f230388cf98113cd62791f"
97
+ "gitHead": "d0e0365ee11fe84fed449bf2b50e6b913f322fc4"
98
98
  }
@@ -16,6 +16,7 @@ const meta: Meta<typeof Box> = {
16
16
  },
17
17
  };
18
18
 
19
+ // oxlint-disable-next-line local/storybook-figma-design-url -- layout primitive, no Figma counterpart
19
20
  export default meta;
20
21
 
21
22
  type Story = StoryObj<typeof Box>;
@@ -27,6 +27,7 @@ const Wrapper = ({ children }: { children: React.ReactNode }) => (
27
27
  </div>
28
28
  );
29
29
 
30
+ // oxlint-disable-next-line local/storybook-figma-design-url -- decorative gradient, no Figma counterpart
30
31
  export default {
31
32
  title: 'UI-Library/ColorWheelGradient',
32
33
  tags: ['deprecated'],
@@ -55,12 +56,6 @@ export default {
55
56
  control: 'number',
56
57
  },
57
58
  },
58
- parameters: {
59
- design: {
60
- type: 'figma',
61
- url: 'TODO: Add Figma URL',
62
- },
63
- },
64
59
  } as Meta<ColorWheelGradientProps>;
65
60
 
66
61
  type Story = StoryObj<ColorWheelGradientProps>;
@@ -26,6 +26,7 @@ const meta: Meta<typeof Image> = {
26
26
  },
27
27
  };
28
28
 
29
+ // oxlint-disable-next-line local/storybook-figma-design-url -- thin <img> wrapper, no Figma counterpart
29
30
  export default meta;
30
31
 
31
32
  type Story = StoryObj<typeof Image>;
@@ -30,6 +30,7 @@ export * from './tabs';
30
30
  export * from './shortcut';
31
31
  export * from './icon_button';
32
32
  export * from './brand_icon';
33
+ export * from './skeleton';
33
34
  export * from './spinner';
34
35
  export * from './store';
35
36
  export * from './checkbox';
@@ -3,6 +3,7 @@ import styled from 'styled-components';
3
3
 
4
4
  import { TableOfContents, type TableOfContentsProps } from './table_of_contents';
5
5
 
6
+ // oxlint-disable-next-line local/storybook-figma-design-url -- no Figma counterpart
6
7
  export default {
7
8
  title: 'UI-Library/TableOfContents',
8
9
  component: TableOfContents,
@@ -0,0 +1,67 @@
1
+ import styled from 'styled-components';
2
+
3
+ import { Box, Skeleton, SkeletonGroup, theme } from '@apify/ui-library';
4
+
5
+ // apify.com Ideas search results: avatar, heading, a three-line paragraph and two footer chips.
6
+
7
+ const StyledGrid = styled(SkeletonGroup)`
8
+ display: grid;
9
+ grid-template-columns: repeat(auto-fill, minmax(26rem, 1fr));
10
+ gap: ${theme.space.space16};
11
+ `;
12
+
13
+ const StyledCard = styled(Box)`
14
+ display: flex;
15
+ flex-direction: column;
16
+ gap: ${theme.space.space16};
17
+ padding: ${theme.space.space16} ${theme.space.space24};
18
+ border: 1px solid ${theme.color.neutral.separatorSubtle};
19
+ border-radius: ${theme.radius.radius16};
20
+ background-color: ${theme.color.neutral.cardBackground};
21
+ `;
22
+
23
+ const StyledHeader = styled(Box)`
24
+ display: flex;
25
+ align-items: center;
26
+ gap: ${theme.space.space8};
27
+ `;
28
+
29
+ const StyledLines = styled(Box)`
30
+ display: flex;
31
+ flex-direction: column;
32
+ gap: ${theme.space.space12};
33
+ `;
34
+
35
+ const StyledFooter = styled(Box)`
36
+ display: flex;
37
+ align-items: center;
38
+ justify-content: space-between;
39
+ gap: ${theme.space.space12};
40
+ margin-top: auto;
41
+ `;
42
+
43
+ const ActorIdeaCardSkeleton = () => (
44
+ <StyledCard>
45
+ <StyledHeader>
46
+ <Skeleton width={theme.space.space40} height={theme.space.space40} />
47
+ <Skeleton width="52%" height="2rem" />
48
+ </StyledHeader>
49
+ <StyledLines>
50
+ <Skeleton height={theme.space.space16} />
51
+ <Skeleton width="94%" height={theme.space.space16} />
52
+ <Skeleton width="68%" height={theme.space.space16} />
53
+ </StyledLines>
54
+ <StyledFooter>
55
+ <Skeleton width="52%" height="2rem" />
56
+ <Skeleton width="34%" height={theme.space.space16} />
57
+ </StyledFooter>
58
+ </StyledCard>
59
+ );
60
+
61
+ export const ActorIdeaCardsSkeleton = ({ count = 6 }: { count?: number }) => (
62
+ <StyledGrid label="Loading Actor ideas">
63
+ {Array.from({ length: count }, (_, index) => (
64
+ <ActorIdeaCardSkeleton key={index} />
65
+ ))}
66
+ </StyledGrid>
67
+ );
@@ -0,0 +1,55 @@
1
+ import styled from 'styled-components';
2
+
3
+ import { Box, Skeleton, SkeletonGroup, theme } from '@apify/ui-library';
4
+
5
+ // Console new Actor wizard: the use-case, language and recommendation cards of a step.
6
+
7
+ const StyledGrid = styled(SkeletonGroup)`
8
+ display: grid;
9
+ grid-template-columns: repeat(auto-fill, minmax(20rem, 1fr));
10
+ gap: ${theme.space.space16};
11
+ `;
12
+
13
+ const StyledCard = styled(Box)`
14
+ display: flex;
15
+ flex-direction: column;
16
+ gap: ${theme.space.space16};
17
+ min-height: 23rem;
18
+ padding: ${theme.space.space24};
19
+ border: 1px solid ${theme.color.neutral.separatorSubtle};
20
+ border-radius: ${theme.radius.radius12};
21
+ background-color: ${theme.color.neutral.cardBackground};
22
+ `;
23
+
24
+ const StyledLines = styled(Box)`
25
+ display: flex;
26
+ flex-direction: column;
27
+ gap: ${theme.space.space4};
28
+ `;
29
+
30
+ const StyledActions = styled(StyledLines)`
31
+ margin-top: auto;
32
+ `;
33
+
34
+ const NewActorWizardCardSkeleton = () => (
35
+ <StyledCard>
36
+ <Skeleton width={theme.space.space32} height={theme.space.space32} />
37
+ <Skeleton width="40%" height={theme.space.space12} />
38
+ <StyledLines>
39
+ <Skeleton width="90%" height={theme.space.space8} />
40
+ <Skeleton width="70%" height={theme.space.space8} />
41
+ </StyledLines>
42
+ <StyledActions>
43
+ <Skeleton height={theme.space.space32} />
44
+ <Skeleton height={theme.space.space32} />
45
+ </StyledActions>
46
+ </StyledCard>
47
+ );
48
+
49
+ export const NewActorWizardCardsSkeleton = ({ count = 4 }: { count?: number }) => (
50
+ <StyledGrid label="Loading recommendations">
51
+ {Array.from({ length: count }, (_, index) => (
52
+ <NewActorWizardCardSkeleton key={index} />
53
+ ))}
54
+ </StyledGrid>
55
+ );
@@ -0,0 +1,50 @@
1
+ import styled from 'styled-components';
2
+
3
+ import { Box, Skeleton, SkeletonGroup, Text, theme } from '@apify/ui-library';
4
+
5
+ // Actor publication tab while the LLM assigns categories: the labels are real text, only the values load.
6
+
7
+ const CATEGORY_ROWS = [
8
+ { label: 'Use case', width: '49%' },
9
+ { label: 'Industry', width: '60%' },
10
+ { label: 'Output', width: '49%' },
11
+ { label: 'Domain', width: '60%' },
12
+ { label: 'Label', width: '55%' },
13
+ ];
14
+
15
+ const StyledPanel = styled(SkeletonGroup)`
16
+ max-width: 48rem;
17
+ border: 1px solid ${theme.color.neutral.separatorSubtle};
18
+ border-radius: ${theme.radius.radius12};
19
+ background-color: ${theme.color.neutral.cardBackground};
20
+ `;
21
+
22
+ const StyledRow = styled(Box)`
23
+ display: flex;
24
+ align-items: center;
25
+ gap: ${theme.space.space16};
26
+ min-height: 5.2rem;
27
+ padding: ${theme.space.space16};
28
+
29
+ & + & {
30
+ border-top: 1px solid ${theme.color.neutral.separatorSubtle};
31
+ }
32
+ `;
33
+
34
+ const StyledLabel = styled(Text)`
35
+ flex: none;
36
+ width: 9.2rem;
37
+ `;
38
+
39
+ export const PublicationCategoriesSkeleton = () => (
40
+ <StyledPanel label="Assigning categories">
41
+ {CATEGORY_ROWS.map(({ label, width }) => (
42
+ <StyledRow key={label}>
43
+ <StyledLabel size="small" color={theme.color.neutral.textSubtle}>
44
+ {label}
45
+ </StyledLabel>
46
+ <Skeleton width={width} height={theme.space.space12} />
47
+ </StyledRow>
48
+ ))}
49
+ </StyledPanel>
50
+ );
@@ -0,0 +1,74 @@
1
+ import styled from 'styled-components';
2
+
3
+ import { Box, SkeletonFadeIn, Text, theme } from '@apify/ui-library';
4
+
5
+ import {
6
+ StoreActorCardsSkeleton,
7
+ StyledStoreCardBody,
8
+ StyledStoreCardFooter,
9
+ StyledStoreCardShell,
10
+ } from './store_actor_cards_skeleton.example';
11
+
12
+ // Show the skeleton only while the request is in flight, swap it for the content the moment the data lands,
13
+ // and let the content fade in. The skeleton mirrors the card, so the swap does not move the page.
14
+
15
+ type StoreActor = { id: string; title: string; name: string; description: string; author: string; stats: string };
16
+
17
+ const StyledGrid = styled(Box)`
18
+ display: grid;
19
+ grid-template-columns: repeat(auto-fill, minmax(28rem, 1fr));
20
+ gap: ${theme.space.space16};
21
+ `;
22
+
23
+ const StyledHeader = styled(Box)`
24
+ display: flex;
25
+ align-items: center;
26
+ gap: ${theme.space.space12};
27
+ `;
28
+
29
+ const StyledAvatar = styled(Box)`
30
+ flex: none;
31
+ width: ${theme.space.space40};
32
+ height: ${theme.space.space40};
33
+ border-radius: ${theme.radius.radius12};
34
+ background-color: ${theme.color.primary.background};
35
+ `;
36
+
37
+ const StoreActorCard = ({ actor }: { actor: StoreActor }) => (
38
+ <StyledStoreCardShell>
39
+ <StyledStoreCardBody>
40
+ <StyledHeader>
41
+ <StyledAvatar />
42
+ <Box>
43
+ <Text weight="bold">{actor.title}</Text>
44
+ <Text size="small" color={theme.color.neutral.textSubtle}>
45
+ {actor.name}
46
+ </Text>
47
+ </Box>
48
+ </StyledHeader>
49
+ <Text size="small">{actor.description}</Text>
50
+ </StyledStoreCardBody>
51
+ <StyledStoreCardFooter>
52
+ <Text size="small" color={theme.color.neutral.textSubtle}>
53
+ {actor.author}
54
+ </Text>
55
+ <Text size="small" color={theme.color.neutral.textSubtle}>
56
+ {actor.stats}
57
+ </Text>
58
+ </StyledStoreCardFooter>
59
+ </StyledStoreCardShell>
60
+ );
61
+
62
+ export const StoreActorCardsLoading = ({ actors }: { actors?: StoreActor[] }) => {
63
+ if (!actors) return <StoreActorCardsSkeleton count={2} />;
64
+
65
+ return (
66
+ <SkeletonFadeIn>
67
+ <StyledGrid>
68
+ {actors.map((actor) => (
69
+ <StoreActorCard key={actor.id} actor={actor} />
70
+ ))}
71
+ </StyledGrid>
72
+ </SkeletonFadeIn>
73
+ );
74
+ };
@@ -0,0 +1,94 @@
1
+ import styled from 'styled-components';
2
+
3
+ import { Box, Skeleton, SkeletonGroup, theme } from '@apify/ui-library';
4
+
5
+ // Store home sections, Store search and Actor collections. The card has two surfaces: the body is inset in a
6
+ // `backgroundSubtle` shell and the footer sits on the shell. The translucent skeleton stays visible on both.
7
+
8
+ const StyledGrid = styled(SkeletonGroup)`
9
+ display: grid;
10
+ grid-template-columns: repeat(auto-fill, minmax(28rem, 1fr));
11
+ gap: ${theme.space.space16};
12
+ `;
13
+
14
+ export const StyledStoreCardShell = styled(Box)`
15
+ display: flex;
16
+ flex-direction: column;
17
+ gap: ${theme.space.space2};
18
+ padding: ${theme.space.space2};
19
+ border-radius: ${theme.radius.radius8};
20
+ background-color: ${theme.color.neutral.backgroundSubtle};
21
+ `;
22
+
23
+ export const StyledStoreCardBody = styled(Box)`
24
+ display: flex;
25
+ flex-direction: column;
26
+ gap: ${theme.space.space8};
27
+ padding: ${theme.space.space16};
28
+ border-radius: ${theme.radius.radius6};
29
+ background-color: ${theme.color.neutral.cardBackground};
30
+ `;
31
+
32
+ export const StyledStoreCardFooter = styled(Box)`
33
+ display: flex;
34
+ align-items: center;
35
+ justify-content: space-between;
36
+ gap: ${theme.space.space8};
37
+ padding: ${theme.space.space4} ${theme.space.space8};
38
+ `;
39
+
40
+ const StyledRow = styled(Box)`
41
+ display: flex;
42
+ align-items: center;
43
+ gap: ${theme.space.space8};
44
+ `;
45
+
46
+ const StyledHeader = styled(StyledRow)`
47
+ gap: ${theme.space.space12};
48
+ `;
49
+
50
+ const StyledTitle = styled(Box)`
51
+ flex: 1;
52
+ min-width: 0;
53
+ `;
54
+
55
+ const StyledLines = styled(Box)`
56
+ display: flex;
57
+ flex-direction: column;
58
+ gap: ${theme.space.space8};
59
+ `;
60
+
61
+ const StoreActorCardSkeleton = () => (
62
+ <StyledStoreCardShell>
63
+ <StyledStoreCardBody>
64
+ <StyledHeader>
65
+ <Skeleton width={theme.space.space40} height={theme.space.space40} />
66
+ <StyledTitle>
67
+ <Skeleton width="70%" height="2rem" />
68
+ </StyledTitle>
69
+ </StyledHeader>
70
+ <StyledLines>
71
+ <Skeleton height={theme.space.space12} />
72
+ <Skeleton width="62%" height={theme.space.space12} />
73
+ </StyledLines>
74
+ </StyledStoreCardBody>
75
+ <StyledStoreCardFooter>
76
+ <StyledRow>
77
+ <Skeleton width="2rem" height="2rem" />
78
+ <Skeleton width="3.8rem" height={theme.space.space16} />
79
+ </StyledRow>
80
+ <StyledRow>
81
+ <Skeleton width="5.6rem" height={theme.space.space16} />
82
+ <Skeleton width={theme.space.space40} height={theme.space.space16} />
83
+ </StyledRow>
84
+ </StyledStoreCardFooter>
85
+ </StyledStoreCardShell>
86
+ );
87
+
88
+ export const StoreActorCardsSkeleton = ({ count = 4 }: { count?: number }) => (
89
+ <StyledGrid label="Loading Actors">
90
+ {Array.from({ length: count }, (_, index) => (
91
+ <StoreActorCardSkeleton key={index} />
92
+ ))}
93
+ </StyledGrid>
94
+ );
@@ -0,0 +1,5 @@
1
+ export { Skeleton, type SkeletonProps } from './skeleton';
2
+ export { SkeletonFadeIn, type SkeletonFadeInProps } from './skeleton_fade_in';
3
+ export { SkeletonGroup, type SkeletonGroupProps } from './skeleton_group';
4
+ export { skeletonClassNames } from './skeleton.styled';
5
+ export { skeletonTestIds } from './skeleton.test_ids';
@@ -0,0 +1,221 @@
1
+ /* eslint-disable import/default -- Vite `?raw` imports have a default export the import plugin cannot see */
2
+ import type { Meta, StoryObj } from '@storybook/react-vite';
3
+ import { useState } from 'react';
4
+ import { expect, userEvent, within } from 'storybook/test';
5
+
6
+ import { theme } from '../../design_system/theme';
7
+ import { Box } from '../box';
8
+ import { Button } from '../button';
9
+ import { Skeleton, SkeletonFadeIn, SkeletonGroup, skeletonTestIds } from './';
10
+ import { ActorIdeaCardsSkeleton } from './examples/actor_idea_cards_skeleton.example';
11
+ import actorIdeaCardsSource from './examples/actor_idea_cards_skeleton.example?raw';
12
+ import { NewActorWizardCardsSkeleton } from './examples/new_actor_wizard_cards_skeleton.example';
13
+ import newActorWizardCardsSource from './examples/new_actor_wizard_cards_skeleton.example?raw';
14
+ import { PublicationCategoriesSkeleton } from './examples/publication_categories_skeleton.example';
15
+ import publicationCategoriesSource from './examples/publication_categories_skeleton.example?raw';
16
+ import { StoreActorCardsLoading } from './examples/store_actor_cards_loading.example';
17
+ import storeActorCardsLoadingSource from './examples/store_actor_cards_loading.example?raw';
18
+ import { StoreActorCardsSkeleton } from './examples/store_actor_cards_skeleton.example';
19
+ import storeActorCardsSource from './examples/store_actor_cards_skeleton.example?raw';
20
+
21
+ // Each story renders a `*.example.tsx` composition and shows that same file as its code, so what developers and
22
+ // agents copy is exactly what is rendered.
23
+
24
+ const meta: Meta<typeof Skeleton> = {
25
+ title: 'UI-Library/Skeleton',
26
+ tags: ['new'],
27
+ component: Skeleton,
28
+ subcomponents: { SkeletonGroup, SkeletonFadeIn },
29
+ parameters: {
30
+ docs: {
31
+ description: {
32
+ component:
33
+ 'Placeholder for content that is loading. Mirror its layout with `Skeleton` blocks inside a ' +
34
+ '`SkeletonGroup`, and wrap the loaded content in `SkeletonFadeIn`. Spec: apify/apify-design#257.\n\n' +
35
+ 'The stories are examples to copy next to the card they mirror.',
36
+ },
37
+ },
38
+ },
39
+ argTypes: {
40
+ width: { control: false },
41
+ height: { control: false },
42
+ },
43
+ };
44
+
45
+ export default meta;
46
+
47
+ type Story = StoryObj<typeof Skeleton>;
48
+
49
+ const withSource = (code: string) => ({ docs: { source: { code, language: 'tsx' } } });
50
+
51
+ // ---------------------------------------------------------------------------
52
+ // Shared assertions
53
+ // ---------------------------------------------------------------------------
54
+
55
+ const prefersReducedMotion = () => window.matchMedia('(prefers-reduced-motion: reduce)').matches;
56
+
57
+ const getLoadingRegion = async (canvasElement: HTMLElement, label: string, blockCount: number) => {
58
+ const region = within(canvasElement).getByRole('status', { name: label });
59
+ await expect(region).toHaveAttribute('data-test', skeletonTestIds.GROUP);
60
+ await expect(region).toHaveAttribute('aria-busy', 'true');
61
+ const blocks = within(region).getAllByTestId(skeletonTestIds.ROOT);
62
+ await expect(blocks).toHaveLength(blockCount);
63
+ return { region, blocks };
64
+ };
65
+
66
+ /** The spec every block follows: 4px radius, decorative, and the wave on `::after` unless motion is reduced. */
67
+ const expectSpec = async (blocks: HTMLElement[]) => {
68
+ for (const block of blocks) {
69
+ await expect(block).toHaveAttribute('aria-hidden', 'true');
70
+ await expect(getComputedStyle(block).borderRadius).toBe(theme.radius.radius4);
71
+ }
72
+ const wave = getComputedStyle(blocks[0], '::after');
73
+ if (prefersReducedMotion()) {
74
+ await expect(wave.content).toBe('none');
75
+ return;
76
+ }
77
+ await expect(wave.animationDuration).toBe('1.6s');
78
+ await expect(wave.animationTimingFunction).toBe('ease-in-out');
79
+ await expect(wave.animationIterationCount).toBe('infinite');
80
+ };
81
+
82
+ /** Background the block is painted over: the nearest ancestor with an opaque background. */
83
+ const getSurfaceColor = (element: HTMLElement) => {
84
+ let node = element.parentElement;
85
+ while (node && getComputedStyle(node).backgroundColor === 'rgba(0, 0, 0, 0)') node = node.parentElement;
86
+ return node ? getComputedStyle(node).backgroundColor : '';
87
+ };
88
+
89
+ // ---------------------------------------------------------------------------
90
+ // Stories
91
+ // ---------------------------------------------------------------------------
92
+
93
+ /**
94
+ * The primitive on its own: one block that fills the width of its container. A block sets only its height
95
+ * (default `1em`), plus a width when a line is shorter than its column.
96
+ */
97
+ export const Default: Story = {
98
+ args: {
99
+ height: theme.space.space16,
100
+ },
101
+ decorators: [
102
+ (Story) => (
103
+ <Box style={{ maxWidth: '32rem' }}>
104
+ <Story />
105
+ </Box>
106
+ ),
107
+ ],
108
+ play: async ({ canvasElement }) => {
109
+ const block = within(canvasElement).getByTestId(skeletonTestIds.ROOT);
110
+ await expectSpec([block]);
111
+ // Without a width, the block fills its container.
112
+ await expect(block.getBoundingClientRect().width).toBe(block.parentElement?.getBoundingClientRect().width);
113
+ },
114
+ };
115
+
116
+ /** apify.com Ideas search results: the richest layout - avatar, heading, paragraph and two footer chips. */
117
+ export const ActorIdeaCards: Story = {
118
+ render: () => <ActorIdeaCardsSkeleton />,
119
+ parameters: withSource(actorIdeaCardsSource),
120
+ play: async ({ canvasElement }) => {
121
+ const { blocks } = await getLoadingRegion(canvasElement, 'Loading Actor ideas', 6 * 7);
122
+ await expectSpec(blocks);
123
+ },
124
+ };
125
+
126
+ /**
127
+ * Store home sections, Store search and Actor collections. The footer sits on the `backgroundSubtle` shell
128
+ * rather than the card body; the translucent skeleton stays visible on both surfaces.
129
+ */
130
+ export const StoreActorCards: Story = {
131
+ render: () => <StoreActorCardsSkeleton />,
132
+ parameters: withSource(storeActorCardsSource),
133
+ play: async ({ canvasElement }) => {
134
+ const { blocks } = await getLoadingRegion(canvasElement, 'Loading Actors', 4 * 8);
135
+ await expectSpec(blocks);
136
+ const [bodyBlock, , , , footerBlock] = blocks;
137
+ for (const block of [bodyBlock, footerBlock]) {
138
+ await expect(getComputedStyle(block).backgroundColor).not.toBe(getSurfaceColor(block));
139
+ }
140
+ await expect(getSurfaceColor(footerBlock)).not.toBe(getSurfaceColor(bodyBlock));
141
+ },
142
+ };
143
+
144
+ /** Actor publication tab while the LLM assigns categories: real labels, loading values. */
145
+ export const PublicationCategories: Story = {
146
+ render: () => <PublicationCategoriesSkeleton />,
147
+ parameters: withSource(publicationCategoriesSource),
148
+ play: async ({ canvasElement }) => {
149
+ const { region, blocks } = await getLoadingRegion(canvasElement, 'Assigning categories', 5);
150
+ await expectSpec(blocks);
151
+ await expect(within(region).getByText('Use case')).toBeVisible();
152
+ },
153
+ };
154
+
155
+ /** Console new Actor wizard: the use-case, language and recommendation cards of a step. */
156
+ export const NewActorWizardCards: Story = {
157
+ render: () => <NewActorWizardCardsSkeleton />,
158
+ parameters: withSource(newActorWizardCardsSource),
159
+ play: async ({ canvasElement }) => {
160
+ const { blocks } = await getLoadingRegion(canvasElement, 'Loading recommendations', 4 * 6);
161
+ await expectSpec(blocks);
162
+ },
163
+ };
164
+
165
+ const STORE_ACTORS = [
166
+ {
167
+ id: '1',
168
+ title: 'Website Content Crawler',
169
+ name: 'apify/website-content-crawler',
170
+ description: 'Crawl websites and extract text content to feed AI models and LLM applications.',
171
+ author: 'Apify',
172
+ stats: '4.6 · 92K users',
173
+ },
174
+ {
175
+ id: '2',
176
+ title: 'Google Maps Scraper',
177
+ name: 'compass/crawler-google-places',
178
+ description: 'Extract data from Google Maps places: reviews, contact details, opening hours and more.',
179
+ author: 'Compass',
180
+ stats: '4.7 · 210K users',
181
+ },
182
+ ];
183
+
184
+ const LoadingToggle = () => {
185
+ const [isLoaded, setIsLoaded] = useState(false);
186
+
187
+ return (
188
+ <Box>
189
+ <Button size="small" variant="secondary" onClick={() => setIsLoaded((value) => !value)} mb="space16">
190
+ {isLoaded ? 'Load again' : 'Finish loading'}
191
+ </Button>
192
+ <StoreActorCardsLoading actors={isLoaded ? STORE_ACTORS : undefined} />
193
+ </Box>
194
+ );
195
+ };
196
+
197
+ /**
198
+ * The skeleton is swapped for the content the moment the data lands, and the content fades in over 80 ms.
199
+ * Because the skeleton mirrors the card, the swap does not move anything on the page.
200
+ */
201
+ export const FadeIn: Story = {
202
+ render: () => <LoadingToggle />,
203
+ parameters: withSource(storeActorCardsLoadingSource),
204
+ play: async ({ canvasElement }) => {
205
+ const canvas = within(canvasElement);
206
+ const { region } = await getLoadingRegion(canvasElement, 'Loading Actors', 2 * 8);
207
+ const skeletonCardHeight = (region.firstElementChild as HTMLElement).getBoundingClientRect().height;
208
+
209
+ await userEvent.click(canvas.getByRole('button', { name: 'Finish loading' }));
210
+
211
+ await expect(canvas.queryByTestId(skeletonTestIds.ROOT)).not.toBeInTheDocument();
212
+ const content = canvas.getByTestId(skeletonTestIds.FADE_IN);
213
+ await expect(content).toHaveTextContent('Website Content Crawler');
214
+ if (!prefersReducedMotion()) {
215
+ await expect(getComputedStyle(content).animationDuration).toBe('0.08s');
216
+ }
217
+ // The skeleton mirrors the card, so the loaded card is about as tall and nothing below it jumps.
218
+ const loadedCard = content.firstElementChild?.firstElementChild as HTMLElement;
219
+ await expect(Math.abs(loadedCard.getBoundingClientRect().height - skeletonCardHeight)).toBeLessThan(16);
220
+ },
221
+ };