@apify/ui-library 1.168.2 → 1.169.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/components/avatar/bordered_actor_avatar.d.ts +9 -0
- package/dist/components/avatar/bordered_actor_avatar.d.ts.map +1 -0
- package/dist/components/avatar/bordered_actor_avatar.js +43 -0
- package/dist/components/avatar/index.d.ts +1 -0
- package/dist/components/avatar/index.d.ts.map +1 -1
- package/dist/components/index.d.ts +1 -0
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/skeleton/index.d.ts +6 -0
- package/dist/components/skeleton/index.d.ts.map +1 -0
- package/dist/components/skeleton/skeleton.d.ts +18 -0
- package/dist/components/skeleton/skeleton.d.ts.map +1 -0
- package/dist/components/skeleton/skeleton.js +19 -0
- package/dist/components/skeleton/skeleton.styled.d.ts +13 -0
- package/dist/components/skeleton/skeleton.styled.d.ts.map +1 -0
- package/dist/components/skeleton/skeleton.styled.js +55 -0
- package/dist/components/skeleton/skeleton.test_ids.d.ts +6 -0
- package/dist/components/skeleton/skeleton.test_ids.d.ts.map +1 -0
- package/dist/components/skeleton/skeleton.test_ids.js +8 -0
- package/dist/components/skeleton/skeleton.type-test.d.ts +2 -0
- package/dist/components/skeleton/skeleton.type-test.d.ts.map +1 -0
- package/dist/components/skeleton/skeleton_fade_in.d.ts +9 -0
- package/dist/components/skeleton/skeleton_fade_in.d.ts.map +1 -0
- package/dist/components/skeleton/skeleton_fade_in.js +15 -0
- package/dist/components/skeleton/skeleton_group.d.ts +13 -0
- package/dist/components/skeleton/skeleton_group.d.ts.map +1 -0
- package/dist/components/skeleton/skeleton_group.js +19 -0
- package/dist/design_system/colors/generated/colors_theme.dark.d.ts +13 -0
- package/dist/design_system/colors/generated/colors_theme.dark.d.ts.map +1 -1
- package/dist/design_system/colors/generated/colors_theme.dark.js +13 -0
- package/dist/design_system/colors/generated/colors_theme.light.d.ts +13 -0
- package/dist/design_system/colors/generated/colors_theme.light.d.ts.map +1 -1
- package/dist/design_system/colors/generated/colors_theme.light.js +13 -0
- package/dist/design_system/theme.d.ts +26 -0
- package/dist/design_system/theme.d.ts.map +1 -1
- package/dist/files_hash_at_build.txt +1 -1
- package/dist/index.js +7 -1
- package/dist/src/components/avatar/bordered_actor_avatar.d.ts +9 -0
- package/dist/src/components/avatar/bordered_actor_avatar.d.ts.map +1 -0
- package/dist/src/components/avatar/bordered_actor_avatar.js +28 -0
- package/dist/src/components/avatar/bordered_actor_avatar.js.map +1 -0
- package/dist/src/components/avatar/index.d.ts +1 -0
- package/dist/src/components/avatar/index.d.ts.map +1 -1
- package/dist/src/components/avatar/index.js +1 -0
- package/dist/src/components/avatar/index.js.map +1 -1
- package/dist/src/components/index.d.ts +1 -0
- package/dist/src/components/index.d.ts.map +1 -1
- package/dist/src/components/index.js +1 -0
- package/dist/src/components/index.js.map +1 -1
- package/dist/src/components/skeleton/index.d.ts +6 -0
- package/dist/src/components/skeleton/index.d.ts.map +1 -0
- package/dist/src/components/skeleton/index.js +6 -0
- package/dist/src/components/skeleton/index.js.map +1 -0
- package/dist/src/components/skeleton/skeleton.d.ts +18 -0
- package/dist/src/components/skeleton/skeleton.d.ts.map +1 -0
- package/dist/src/components/skeleton/skeleton.js +10 -0
- package/dist/src/components/skeleton/skeleton.js.map +1 -0
- package/dist/src/components/skeleton/skeleton.styled.d.ts +13 -0
- package/dist/src/components/skeleton/skeleton.styled.d.ts.map +1 -0
- package/dist/src/components/skeleton/skeleton.styled.js +55 -0
- package/dist/src/components/skeleton/skeleton.styled.js.map +1 -0
- package/dist/src/components/skeleton/skeleton.test_ids.d.ts +6 -0
- package/dist/src/components/skeleton/skeleton.test_ids.d.ts.map +1 -0
- package/dist/src/components/skeleton/skeleton.test_ids.js +6 -0
- package/dist/src/components/skeleton/skeleton.test_ids.js.map +1 -0
- package/dist/src/components/skeleton/skeleton.type-test.d.ts +2 -0
- package/dist/src/components/skeleton/skeleton.type-test.d.ts.map +1 -0
- package/dist/src/components/skeleton/skeleton.type-test.js +25 -0
- package/dist/src/components/skeleton/skeleton.type-test.js.map +1 -0
- package/dist/src/components/skeleton/skeleton_fade_in.d.ts +9 -0
- package/dist/src/components/skeleton/skeleton_fade_in.d.ts.map +1 -0
- package/dist/src/components/skeleton/skeleton_fade_in.js +9 -0
- package/dist/src/components/skeleton/skeleton_fade_in.js.map +1 -0
- package/dist/src/components/skeleton/skeleton_group.d.ts +13 -0
- package/dist/src/components/skeleton/skeleton_group.d.ts.map +1 -0
- package/dist/src/components/skeleton/skeleton_group.js +10 -0
- package/dist/src/components/skeleton/skeleton_group.js.map +1 -0
- package/dist/src/design_system/colors/generated/colors_theme.dark.d.ts +13 -0
- package/dist/src/design_system/colors/generated/colors_theme.dark.d.ts.map +1 -1
- package/dist/src/design_system/colors/generated/colors_theme.dark.js +13 -0
- package/dist/src/design_system/colors/generated/colors_theme.dark.js.map +1 -1
- package/dist/src/design_system/colors/generated/colors_theme.light.d.ts +13 -0
- package/dist/src/design_system/colors/generated/colors_theme.light.d.ts.map +1 -1
- package/dist/src/design_system/colors/generated/colors_theme.light.js +13 -0
- package/dist/src/design_system/colors/generated/colors_theme.light.js.map +1 -1
- package/dist/src/design_system/theme.d.ts +26 -0
- package/dist/src/design_system/theme.d.ts.map +1 -1
- package/dist/src/test_ids.d.ts +1 -0
- package/dist/src/test_ids.d.ts.map +1 -1
- package/dist/src/test_ids.js +1 -0
- package/dist/src/test_ids.js.map +1 -1
- package/dist/test_ids.d.ts +1 -0
- package/dist/test_ids.d.ts.map +1 -1
- package/dist/test_ids.js +2 -1
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +3 -3
- package/src/components/avatar/bordered_actor_avatar.stories.tsx +54 -0
- package/src/components/avatar/bordered_actor_avatar.tsx +45 -0
- package/src/components/avatar/index.ts +1 -0
- package/src/components/index.ts +1 -0
- package/src/components/skeleton/examples/actor_idea_cards_skeleton.example.tsx +67 -0
- package/src/components/skeleton/examples/new_actor_wizard_cards_skeleton.example.tsx +55 -0
- package/src/components/skeleton/examples/publication_categories_skeleton.example.tsx +50 -0
- package/src/components/skeleton/examples/store_actor_cards_loading.example.tsx +74 -0
- package/src/components/skeleton/examples/store_actor_cards_skeleton.example.tsx +94 -0
- package/src/components/skeleton/index.ts +5 -0
- package/src/components/skeleton/skeleton.stories.tsx +221 -0
- package/src/components/skeleton/skeleton.styled.ts +66 -0
- package/src/components/skeleton/skeleton.test_ids.ts +5 -0
- package/src/components/skeleton/skeleton.tsx +34 -0
- package/src/components/skeleton/skeleton.type-test.ts +32 -0
- package/src/components/skeleton/skeleton_fade_in.tsx +21 -0
- package/src/components/skeleton/skeleton_group.tsx +32 -0
- package/src/design_system/colors/build_color_tokens.js +4 -2
- package/src/design_system/colors/colors.stories.tsx +9 -1
- package/src/design_system/colors/generated/colors_theme.dark.ts +13 -0
- package/src/design_system/colors/generated/colors_theme.light.ts +13 -0
- package/src/test_ids.ts +1 -0
|
@@ -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
|
+
};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import styled, { keyframes } from 'styled-components';
|
|
2
|
+
|
|
3
|
+
import { theme } from '../../design_system/theme';
|
|
4
|
+
import { Box } from '../box';
|
|
5
|
+
|
|
6
|
+
export const skeletonClassNames = {
|
|
7
|
+
ROOT: 'Skeleton',
|
|
8
|
+
GROUP: 'Skeleton-Group',
|
|
9
|
+
FADE_IN: 'Skeleton-FadeIn',
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
// TODO: move both durations to design tokens (`theme.transition`) once the motion tokens are agreed.
|
|
13
|
+
const WAVE_DURATION = '1600ms';
|
|
14
|
+
const FADE_IN_DURATION = '80ms';
|
|
15
|
+
|
|
16
|
+
/* The highlight sweeps each block's own width, so short and long blocks start and end together. */
|
|
17
|
+
const wave = keyframes`
|
|
18
|
+
from { transform: translateX(-100%); }
|
|
19
|
+
to { transform: translateX(100%); }
|
|
20
|
+
`;
|
|
21
|
+
|
|
22
|
+
const fadeIn = keyframes`
|
|
23
|
+
from { opacity: 0; }
|
|
24
|
+
to { opacity: 1; }
|
|
25
|
+
`;
|
|
26
|
+
|
|
27
|
+
type StyledSkeletonProps = {
|
|
28
|
+
$width: string;
|
|
29
|
+
$height: string;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export const StyledSkeleton = styled(Box)<StyledSkeletonProps>`
|
|
33
|
+
position: relative;
|
|
34
|
+
overflow: hidden;
|
|
35
|
+
flex: 0 1 auto;
|
|
36
|
+
min-width: 0;
|
|
37
|
+
width: ${({ $width }) => $width};
|
|
38
|
+
height: ${({ $height }) => $height};
|
|
39
|
+
border-radius: ${theme.radius.radius4};
|
|
40
|
+
/* Base and highlight are both on-surface: the highlight is a second translucent layer over the
|
|
41
|
+
base, so one token gives a visible sweep on every surface, in light and dark alike. */
|
|
42
|
+
background-color: ${theme.color.neutral.onSurface};
|
|
43
|
+
|
|
44
|
+
&::after {
|
|
45
|
+
content: '';
|
|
46
|
+
position: absolute;
|
|
47
|
+
inset: 0;
|
|
48
|
+
background: linear-gradient(90deg, transparent, ${theme.color.neutral.onSurface}, transparent);
|
|
49
|
+
transform: translateX(-100%);
|
|
50
|
+
animation: ${wave} ${WAVE_DURATION} ease-in-out infinite;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
@media (prefers-reduced-motion: reduce) {
|
|
54
|
+
&::after {
|
|
55
|
+
content: none;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
`;
|
|
59
|
+
|
|
60
|
+
export const StyledSkeletonFadeIn = styled(Box)`
|
|
61
|
+
animation: ${fadeIn} ${FADE_IN_DURATION} ease-out both;
|
|
62
|
+
|
|
63
|
+
@media (prefers-reduced-motion: reduce) {
|
|
64
|
+
animation: none;
|
|
65
|
+
}
|
|
66
|
+
`;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import clsx from 'clsx';
|
|
2
|
+
import type { ComponentPropsWithoutRef } from 'react';
|
|
3
|
+
|
|
4
|
+
import type { MarginSpacingProps } from '../box';
|
|
5
|
+
import { StyledSkeleton, skeletonClassNames } from './skeleton.styled';
|
|
6
|
+
import { skeletonTestIds } from './skeleton.test_ids';
|
|
7
|
+
|
|
8
|
+
// Native onClick clashes with the legacy BoxProps handler type, so it is omitted.
|
|
9
|
+
export interface SkeletonProps
|
|
10
|
+
extends Omit<ComponentPropsWithoutRef<'div'>, 'children' | 'onClick'>, MarginSpacingProps {
|
|
11
|
+
/**
|
|
12
|
+
* Any CSS length. Defaults to the full width of the container.
|
|
13
|
+
*/
|
|
14
|
+
width?: string;
|
|
15
|
+
/**
|
|
16
|
+
* Any CSS length. Defaults to `1em`, the font size of the surrounding text.
|
|
17
|
+
*/
|
|
18
|
+
height?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Placeholder block shown only while data is loading. Compose blocks to mirror the layout of the content,
|
|
23
|
+
* inside a `SkeletonGroup`.
|
|
24
|
+
*/
|
|
25
|
+
export const Skeleton = ({ width = '100%', height = '1em', className, ...rest }: SkeletonProps) => (
|
|
26
|
+
<StyledSkeleton
|
|
27
|
+
aria-hidden
|
|
28
|
+
data-test={skeletonTestIds.ROOT}
|
|
29
|
+
className={clsx(skeletonClassNames.ROOT, className)}
|
|
30
|
+
$width={width}
|
|
31
|
+
$height={height}
|
|
32
|
+
{...rest}
|
|
33
|
+
/>
|
|
34
|
+
);
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { expectTypeOf } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import type { SkeletonFadeInProps, SkeletonGroupProps, SkeletonProps } from './';
|
|
4
|
+
|
|
5
|
+
// Extends the native <div> element.
|
|
6
|
+
expectTypeOf<SkeletonProps>().toHaveProperty('title');
|
|
7
|
+
expectTypeOf<SkeletonProps>().toHaveProperty('style');
|
|
8
|
+
expectTypeOf<SkeletonFadeInProps>().toHaveProperty('aria-live');
|
|
9
|
+
|
|
10
|
+
// Own props and margin spacing props.
|
|
11
|
+
expectTypeOf<SkeletonProps>().toHaveProperty('width');
|
|
12
|
+
expectTypeOf<SkeletonProps>().toHaveProperty('height');
|
|
13
|
+
expectTypeOf<SkeletonProps>().toHaveProperty('mb');
|
|
14
|
+
|
|
15
|
+
// Every block has the same 4px radius, so there is no radius prop.
|
|
16
|
+
expectTypeOf<SkeletonProps>().not.toHaveProperty('radius');
|
|
17
|
+
|
|
18
|
+
// A skeleton stands in for content, it never holds any.
|
|
19
|
+
expectTypeOf<SkeletonProps>().not.toHaveProperty('children');
|
|
20
|
+
expectTypeOf<SkeletonProps>().not.toHaveProperty('p');
|
|
21
|
+
expectTypeOf<SkeletonFadeInProps>().toHaveProperty('children');
|
|
22
|
+
|
|
23
|
+
// Neither component is interactive.
|
|
24
|
+
expectTypeOf<SkeletonProps>().not.toHaveProperty('onClick');
|
|
25
|
+
expectTypeOf<SkeletonFadeInProps>().not.toHaveProperty('onClick');
|
|
26
|
+
|
|
27
|
+
// The group sets its own accessibility attributes and requires a label.
|
|
28
|
+
expectTypeOf<SkeletonGroupProps>().toHaveProperty('label');
|
|
29
|
+
expectTypeOf<SkeletonGroupProps['label']>().toEqualTypeOf<string>();
|
|
30
|
+
expectTypeOf<SkeletonGroupProps>().not.toHaveProperty('role');
|
|
31
|
+
expectTypeOf<SkeletonGroupProps>().not.toHaveProperty('aria-busy');
|
|
32
|
+
expectTypeOf<SkeletonGroupProps>().not.toHaveProperty('aria-label');
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import clsx from 'clsx';
|
|
2
|
+
import type { ComponentPropsWithoutRef } from 'react';
|
|
3
|
+
|
|
4
|
+
import type { MarginSpacingProps, PaddingSpacingProps } from '../box';
|
|
5
|
+
import { StyledSkeletonFadeIn, skeletonClassNames } from './skeleton.styled';
|
|
6
|
+
import { skeletonTestIds } from './skeleton.test_ids';
|
|
7
|
+
|
|
8
|
+
// Native onClick clashes with the legacy BoxProps handler type, so it is omitted.
|
|
9
|
+
export interface SkeletonFadeInProps
|
|
10
|
+
extends Omit<ComponentPropsWithoutRef<'div'>, 'onClick'>, MarginSpacingProps, PaddingSpacingProps {}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Wraps the content that replaces a skeleton, so it fades in once instead of popping in.
|
|
14
|
+
*/
|
|
15
|
+
export const SkeletonFadeIn = ({ className, ...rest }: SkeletonFadeInProps) => (
|
|
16
|
+
<StyledSkeletonFadeIn
|
|
17
|
+
data-test={skeletonTestIds.FADE_IN}
|
|
18
|
+
className={clsx(skeletonClassNames.FADE_IN, className)}
|
|
19
|
+
{...rest}
|
|
20
|
+
/>
|
|
21
|
+
);
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import clsx from 'clsx';
|
|
2
|
+
import type { ComponentPropsWithoutRef } from 'react';
|
|
3
|
+
|
|
4
|
+
import { Box, type MarginSpacingProps, type PaddingSpacingProps } from '../box';
|
|
5
|
+
import { skeletonClassNames } from './skeleton.styled';
|
|
6
|
+
import { skeletonTestIds } from './skeleton.test_ids';
|
|
7
|
+
|
|
8
|
+
// Native onClick clashes with the legacy BoxProps handler type, so it is omitted.
|
|
9
|
+
export interface SkeletonGroupProps
|
|
10
|
+
extends
|
|
11
|
+
Omit<ComponentPropsWithoutRef<'div'>, 'onClick' | 'role' | 'aria-busy' | 'aria-label'>,
|
|
12
|
+
MarginSpacingProps,
|
|
13
|
+
PaddingSpacingProps {
|
|
14
|
+
/**
|
|
15
|
+
* What is loading, used as the accessible name of the region, e.g. "Loading Actors".
|
|
16
|
+
*/
|
|
17
|
+
label: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Wraps the skeleton blocks of one loading region, names it and marks it busy for assistive technology.
|
|
22
|
+
*/
|
|
23
|
+
export const SkeletonGroup = ({ label, className, ...rest }: SkeletonGroupProps) => (
|
|
24
|
+
<Box
|
|
25
|
+
role="status"
|
|
26
|
+
aria-busy
|
|
27
|
+
aria-label={label}
|
|
28
|
+
data-test={skeletonTestIds.GROUP}
|
|
29
|
+
className={clsx(skeletonClassNames.GROUP, className)}
|
|
30
|
+
{...rest}
|
|
31
|
+
/>
|
|
32
|
+
);
|
|
@@ -110,10 +110,12 @@ StyleDictionary.registerFilter({
|
|
|
110
110
|
filter: (token) => ['Palette'].includes(token.path[0]),
|
|
111
111
|
});
|
|
112
112
|
|
|
113
|
-
// Select only tokens for theme colors (Decorative and
|
|
113
|
+
// Select only tokens for theme colors (Decorative, Palette, Brand and Third Party)
|
|
114
114
|
StyleDictionary.registerFilter({
|
|
115
115
|
name: 'only-theme',
|
|
116
|
-
filter: (token) =>
|
|
116
|
+
filter: (token) =>
|
|
117
|
+
['Decorative', 'Palette', 'Brand'].includes(token.path[0]) ||
|
|
118
|
+
(token.path[0] === 'Semantic' && token.path[1] === 'Third_Party'),
|
|
117
119
|
});
|
|
118
120
|
|
|
119
121
|
// Select only the semantic neutral tokens (used for the brand dark neutral override)
|
|
@@ -36,6 +36,13 @@ const decorativePaletteFamilies = [
|
|
|
36
36
|
'slate',
|
|
37
37
|
];
|
|
38
38
|
const brandPaletteFamilies = ['magenta', 'mondo', 'lime', 'meteorite', 'logo'];
|
|
39
|
+
// Semantic tokens are labelled only by these families, so other theme values (e.g. third-party brand colors) can't take over the labels.
|
|
40
|
+
const labelPaletteFamilies = [
|
|
41
|
+
...basePaletteFamilies,
|
|
42
|
+
...neutralPaletteFamilies,
|
|
43
|
+
...decorativePaletteFamilies,
|
|
44
|
+
...brandPaletteFamilies,
|
|
45
|
+
];
|
|
39
46
|
const semanticColorSections: SemanticColorSection[] = [
|
|
40
47
|
{
|
|
41
48
|
title: 'Neutral',
|
|
@@ -173,7 +180,8 @@ const getColorDisplayName = (name: string, prefix: string) => {
|
|
|
173
180
|
|
|
174
181
|
const getPaletteNameByValue = (value: string, palette: Record<string, string>) => {
|
|
175
182
|
const paletteColor = Object.entries(palette).find(
|
|
176
|
-
([, paletteValue]) =>
|
|
183
|
+
([name, paletteValue]) =>
|
|
184
|
+
labelPaletteFamilies.includes(getPaletteFamily(name)) && paletteValue.toLowerCase() === value.toLowerCase(),
|
|
177
185
|
);
|
|
178
186
|
|
|
179
187
|
return paletteColor ? formatLabel(paletteColor[0]) : 'Color';
|
|
@@ -3,6 +3,19 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
export const darkTheme = {
|
|
6
|
+
thirdPartyAnthropic: '#d97757',
|
|
7
|
+
thirdPartyChatGpt: '#ffffff',
|
|
8
|
+
thirdPartyClaude: '#d97757',
|
|
9
|
+
thirdPartyCursor: '#ffffff',
|
|
10
|
+
thirdPartyDiscord: '#5865f2',
|
|
11
|
+
thirdPartyGithub: '#ffffff',
|
|
12
|
+
thirdPartyLinkedin: '#0a66c2',
|
|
13
|
+
thirdPartyMake: '#6d00cc',
|
|
14
|
+
thirdPartyN8n: '#ea4b71',
|
|
15
|
+
thirdPartyPerplexity: '#20808d',
|
|
16
|
+
thirdPartyTwitter: '#ffffff',
|
|
17
|
+
thirdPartyYoutube: '#ff0000',
|
|
18
|
+
thirdPartyZapier: '#ff4a00',
|
|
6
19
|
magentaDeep: '#694d6b',
|
|
7
20
|
magentaMain: '#9d829f',
|
|
8
21
|
magentaTint: '#fef3ff',
|
|
@@ -3,6 +3,19 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
export const lightTheme = {
|
|
6
|
+
thirdPartyAnthropic: '#d97757',
|
|
7
|
+
thirdPartyChatGpt: '#000000',
|
|
8
|
+
thirdPartyClaude: '#d97757',
|
|
9
|
+
thirdPartyCursor: '#000000',
|
|
10
|
+
thirdPartyDiscord: '#5865f2',
|
|
11
|
+
thirdPartyGithub: '#181717',
|
|
12
|
+
thirdPartyLinkedin: '#0a66c2',
|
|
13
|
+
thirdPartyMake: '#6d00cc',
|
|
14
|
+
thirdPartyN8n: '#ea4b71',
|
|
15
|
+
thirdPartyPerplexity: '#20808d',
|
|
16
|
+
thirdPartyTwitter: '#000000',
|
|
17
|
+
thirdPartyYoutube: '#ff0000',
|
|
18
|
+
thirdPartyZapier: '#ff4a00',
|
|
6
19
|
magentaDeep: '#694d6b',
|
|
7
20
|
magentaMain: '#9d829f',
|
|
8
21
|
magentaTint: '#fef3ff',
|
package/src/test_ids.ts
CHANGED
|
@@ -16,6 +16,7 @@ export {
|
|
|
16
16
|
selectControlTestId,
|
|
17
17
|
selectMultiValueLabelTestId,
|
|
18
18
|
} from './components/select/select.test_ids.js';
|
|
19
|
+
export { skeletonTestIds } from './components/skeleton/skeleton.test_ids.js';
|
|
19
20
|
export { spinnerTestIds } from './components/spinner/spinner.test_ids.js';
|
|
20
21
|
export {
|
|
21
22
|
splitButtonMainActionTestId,
|