Skip to content

Commit 50638fe

Browse files
authored
Merge pull request #3414 from ably/dx-1128/docs-producttile
DX-1128: localise ProductTile + product data (leaf)
2 parents e858242 + 68c7b61 commit 50638fe

10 files changed

Lines changed: 426 additions & 5 deletions

File tree

src/components/Examples/ExamplesContent.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { ImageProps } from '../Image';
66
import { examples, products } from '../../data/examples/';
77
import { filterSearchExamples } from './filter-search-examples';
88
import ExamplesNoResults from './ExamplesNoResults';
9-
import { ProductName } from '@ably/ui/core/ProductTile/data';
9+
import { ProductName } from 'src/components/ui/ProductTile/data';
1010
import { useLocation } from '@reach/router';
1111

1212
export type SelectedFilters = { products: ProductName[]; useCases: string[] };

src/components/Examples/ExamplesFilter.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import ExamplesCheckbox from './ExamplesCheckbox';
99
import { SelectedFilters } from './ExamplesContent';
1010
import { useOnClickOutside } from 'src/hooks/use-on-click-outside';
1111
import { navigate } from 'gatsby';
12-
import { ProductName } from '@ably/ui/core/ProductTile/data';
12+
import { ProductName } from 'src/components/ui/ProductTile/data';
1313
import { AdjustmentsHorizontalIcon, MagnifyingGlassIcon, XMarkIcon } from '@heroicons/react/24/outline';
1414

1515
// Matches Tailwind's `sm` screen (768px), where the filter switches from the

src/components/Examples/ExamplesGrid.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import React, { useCallback } from 'react';
22
import Badge from 'src/components/ui/Badge';
33
import Icon from 'src/components/Icon';
44
import { IconName } from 'src/components/Icon/types';
5-
import { ProductName, products as dataProducts } from '@ably/ui/core/ProductTile/data';
5+
import { ProductName, products as dataProducts } from 'src/components/ui/ProductTile/data';
66
import cn from 'src/utilities/cn';
77
import { Image, ImageProps } from '../Image';
88
import { DEFAULT_EXAMPLE_LANGUAGES } from '../../data/examples/';

src/components/Homepage/PlatformAndProducts.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import ProductTile from '@ably/ui/core/ProductTile';
1+
import ProductTile from 'src/components/ui/ProductTile';
22
import cn from 'src/utilities/cn';
33
import { Image, ImageProps, getImageFromList } from 'src/components/Image';
44
import type { PlatformProductsSectionData, PlatformCardData } from 'src/data/content/types';

src/components/ui/ProductTile.tsx

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
import React from 'react';
2+
import { ArrowRightIcon } from '@heroicons/react/16/solid';
3+
import cn from 'src/utilities/cn';
4+
import { IconSize } from 'src/components/Icon/types';
5+
import LinkButton from './LinkButton';
6+
import { ProductName, products } from './ProductTile/data';
7+
import ProductIcon from './ProductTile/ProductIcon';
8+
import ProductLabel from './ProductTile/ProductLabel';
9+
import ProductDescription from './ProductTile/ProductDescription';
10+
11+
/**
12+
* Props for the ProductTile component.
13+
*/
14+
export type ProductTileProps = {
15+
/**
16+
* The name of the product.
17+
*/
18+
name: ProductName;
19+
20+
/**
21+
* Indicates if the product tile is selected. If `undefined`, the product tile is not selectable.
22+
* @default false
23+
*/
24+
selected?: boolean;
25+
26+
/**
27+
* Indicates if the product tile is on the "current" page. Changes CTA copy.
28+
* @default false
29+
*/
30+
currentPage?: boolean;
31+
32+
/**
33+
* Additional CSS class names to apply to the product tile outer container.
34+
*/
35+
className?: string;
36+
37+
/**
38+
* Additional CSS class names to apply to the product description container.
39+
*/
40+
descriptionClassName?: string;
41+
42+
/**
43+
* Additional CSS class names to apply to the product name / label container.
44+
*/
45+
labelClassName?: string;
46+
47+
/**
48+
* Additional CSS class names to apply to the product icon.
49+
*/
50+
iconClassName?: string;
51+
52+
/**
53+
* Callback function to handle click events on the product tile.
54+
*/
55+
56+
onClick?: () => void;
57+
58+
/**
59+
* Indicates if the product description should be shown.
60+
* @default true
61+
*/
62+
showDescription?: boolean;
63+
64+
/**
65+
* Indicates if the product label should be shown.
66+
* @default true
67+
*/
68+
showLabel?: boolean;
69+
70+
/**
71+
* The size of the product icon.
72+
* @default "40px"
73+
*/
74+
size?: IconSize;
75+
76+
/**
77+
* Indicates if the product icons should be animated.
78+
* @default false
79+
*/
80+
animateIcons?: boolean;
81+
};
82+
83+
const CONTAINER_GAP_RATIO = 3;
84+
85+
const ProductTile = ({
86+
name,
87+
selected,
88+
currentPage,
89+
className,
90+
onClick,
91+
showDescription = true,
92+
showLabel = true,
93+
size = '40px',
94+
animateIcons = false,
95+
descriptionClassName,
96+
labelClassName,
97+
iconClassName,
98+
}: ProductTileProps) => {
99+
const { icon, hoverIcon, label, description, link, unavailable } = products[name] ?? {};
100+
const numericalSize = parseInt(size, 10);
101+
const containerPresent = showDescription || showLabel;
102+
103+
return (
104+
<div
105+
className={cn(
106+
'transition-colors group/product-tile',
107+
{ 'flex flex-col p-3 rounded-lg gap-2': containerPresent },
108+
{ 'bg-neutral-1300 dark:bg-neutral-000': selected },
109+
{
110+
'bg-neutral-000 dark:bg-neutral-1300': !selected,
111+
},
112+
{
113+
'hover:bg-neutral-100 dark:hover:bg-neutral-1200': selected === false && !unavailable,
114+
},
115+
{ 'cursor-pointer': selected !== undefined && !unavailable },
116+
{ 'pointer-events-none': unavailable },
117+
{ [`${className}`]: className },
118+
)}
119+
aria-hidden={unavailable}
120+
onClick={onClick}
121+
onKeyDown={
122+
onClick
123+
? (e) => {
124+
if (e.key === 'Enter' || e.key === ' ') {
125+
e.preventDefault();
126+
onClick();
127+
}
128+
}
129+
: undefined
130+
}
131+
role={onClick ? 'button' : undefined}
132+
tabIndex={onClick ? 0 : undefined}
133+
>
134+
<div
135+
className={cn('items-center', { flex: containerPresent }, { 'inline-flex': !containerPresent })}
136+
style={{
137+
gap: containerPresent ? numericalSize / CONTAINER_GAP_RATIO : 0,
138+
}}
139+
>
140+
<ProductIcon
141+
size={numericalSize}
142+
name={icon}
143+
hoverName={animateIcons ? hoverIcon : undefined}
144+
selected={selected}
145+
unavailable={!!unavailable}
146+
className={iconClassName}
147+
/>
148+
<ProductLabel
149+
label={label}
150+
selected={selected}
151+
unavailable={!!unavailable}
152+
numericalSize={numericalSize}
153+
showLabel={showLabel}
154+
className={labelClassName}
155+
/>
156+
</div>
157+
<ProductDescription
158+
description={description}
159+
selected={selected}
160+
unavailable={!!unavailable}
161+
showDescription={showDescription}
162+
className={descriptionClassName}
163+
/>
164+
{selected && link ? (
165+
<LinkButton
166+
variant="secondary"
167+
size="xs"
168+
className="mt-2 !text-neutral-000 dark:!text-neutral-1300"
169+
rightIcon={<ArrowRightIcon aria-hidden />}
170+
iconColor="text-orange-600"
171+
href={link}
172+
>
173+
{currentPage ? 'View docs' : 'Explore'}
174+
</LinkButton>
175+
) : null}
176+
</div>
177+
);
178+
};
179+
180+
export default ProductTile;
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import React from 'react';
2+
import cn from 'src/utilities/cn';
3+
4+
type ProductDescriptionProps = {
5+
description: string;
6+
selected?: boolean;
7+
unavailable: boolean;
8+
showDescription?: boolean;
9+
className?: string;
10+
};
11+
12+
const ProductDescription = ({
13+
description,
14+
selected,
15+
unavailable,
16+
showDescription = true,
17+
className,
18+
}: ProductDescriptionProps) => {
19+
if (!description || !showDescription) {
20+
return null;
21+
}
22+
23+
return (
24+
<span
25+
className={cn(
26+
'block ui-text-p3 font-medium leading-snug',
27+
{
28+
'text-neutral-300 dark:text-neutral-1000': selected && !unavailable,
29+
},
30+
{
31+
'text-neutral-800 dark:text-neutral-500 group-hover/product-tile:text-neutral-1000 dark:group-hover/product-tile:text-neutral-300':
32+
!selected,
33+
},
34+
className,
35+
)}
36+
>
37+
{description}
38+
</span>
39+
);
40+
};
41+
42+
export default ProductDescription;
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import React from 'react';
2+
import Icon from 'src/components/Icon';
3+
import { IconName } from 'src/components/Icon/types';
4+
import cn from 'src/utilities/cn';
5+
6+
type ProductIconProps = {
7+
name?: IconName;
8+
hoverName?: IconName;
9+
selected?: boolean;
10+
size: number;
11+
unavailable: boolean;
12+
className?: string;
13+
};
14+
15+
const ProductIcon = ({ name, hoverName, selected, size, unavailable, className }: ProductIconProps) => {
16+
if (!name) {
17+
return null;
18+
}
19+
20+
// Padding around the icon is 1/4 the icon's size, so the icon is 4 of 6 parts
21+
const iconSize = (size / 6) * 4;
22+
23+
return (
24+
// Carries the gradient stroke; CSS has no native gradient border.
25+
<span
26+
className={cn(
27+
'flex items-center justify-center border border-neutral-300 dark:border-neutral-1000 rounded-xl',
28+
'bg-neutral-100 dark:bg-neutral-1200 hover:bg-neutral-000 dark:hover:bg-neutral-1300 active:bg-neutral-000 dark:active:bg-neutral-1300',
29+
className,
30+
)}
31+
style={{ width: size, height: size }}
32+
>
33+
{hoverName ? (
34+
<Icon
35+
name={hoverName}
36+
size={`${iconSize}px`}
37+
additionalCSS={cn({
38+
'hidden group-hover/product-tile:flex': !selected,
39+
flex: selected,
40+
})}
41+
/>
42+
) : null}
43+
<Icon
44+
name={name}
45+
size={`${iconSize}px`}
46+
additionalCSS={cn({
47+
'text-neutral-000 dark:text-neutral-1300': selected && !unavailable,
48+
'text-neutral-1300 dark:text-neutral-000': !selected && !unavailable,
49+
'text-neutral-700 dark:text-neutral-600': selected && unavailable,
50+
'text-neutral-600 dark:text-neutral-700': !selected && unavailable,
51+
'flex group-hover/product-tile:hidden': hoverName && !selected,
52+
hidden: hoverName && selected,
53+
})}
54+
/>
55+
</span>
56+
);
57+
};
58+
59+
export default ProductIcon;
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import React from 'react';
2+
import cn from 'src/utilities/cn';
3+
4+
type ProductLabelProps = {
5+
label: string;
6+
unavailable: boolean;
7+
selected?: boolean;
8+
numericalSize: number;
9+
showLabel?: boolean;
10+
className?: string;
11+
};
12+
13+
const LABEL_FONT_SIZE_RATIO = 4;
14+
const NAME_FONT_SIZE_RATIO = 2.6;
15+
16+
const ProductLabel = ({ label, unavailable, selected, numericalSize, showLabel, className }: ProductLabelProps) => {
17+
if (!label || !showLabel) {
18+
return null;
19+
}
20+
21+
const dynamicFontSize = numericalSize / LABEL_FONT_SIZE_RATIO;
22+
23+
return (
24+
<span className="flex flex-col justify-center">
25+
{unavailable ? (
26+
<span className="block">
27+
<span
28+
className="table-cell font-sans bg-neutral-300 dark:bg-neutral-1000 rounded-full text-gui-disabled-light dark:text-gui-disabled-dark tracking-[0.04em] font-bold leading-snug"
29+
style={{
30+
fontSize: dynamicFontSize * 0.6,
31+
padding: `${dynamicFontSize * 0.25}px ${dynamicFontSize * 0.5}px`,
32+
}}
33+
>
34+
COMING SOON
35+
</span>
36+
</span>
37+
) : (
38+
<span
39+
className={cn(
40+
'block font-bold uppercase ui-text-p2 leading-snug',
41+
{ 'text-neutral-500 dark:text-neutral-700': selected },
42+
{ 'text-neutral-700 dark:text-neutral-500': !selected },
43+
)}
44+
style={{
45+
fontSize: dynamicFontSize,
46+
letterSpacing: '0.06em',
47+
}}
48+
>
49+
Ably
50+
</span>
51+
)}
52+
<span
53+
className={cn(
54+
'block ui-text-p2 font-bold',
55+
{
56+
'text-neutral-000 dark:text-neutral-1300': selected === true && !unavailable,
57+
},
58+
{
59+
'text-neutral-1000 dark:text-neutral-300 group-hover/product-tile:text-neutral-1300 dark:group-hover/product-tile:text-neutral-000':
60+
selected === false && !unavailable,
61+
},
62+
{
63+
'text-neutral-1300 dark:text-neutral-000': selected === undefined && !unavailable,
64+
},
65+
{
66+
'text-neutral-700 dark:text-neutral-600': unavailable,
67+
},
68+
{ 'mt-[-3px]': !unavailable },
69+
className,
70+
)}
71+
style={{ fontSize: numericalSize / NAME_FONT_SIZE_RATIO }}
72+
>
73+
{label}
74+
</span>
75+
</span>
76+
);
77+
};
78+
79+
export default ProductLabel;

0 commit comments

Comments
 (0)