Skip to content

Commit

Permalink
feat: v5 icons
Browse files Browse the repository at this point in the history
  • Loading branch information
denkristoffer committed Jan 10, 2024
1 parent d60e32e commit 97288af
Show file tree
Hide file tree
Showing 196 changed files with 906 additions and 2,959 deletions.
595 changes: 278 additions & 317 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion packages/components/icon/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@contentful/f36-icon",
"version": "4.58.0",
"version": "5.0.0-alpha.0",
"description": "Forma 36: Icon component",
"license": "MIT",
"scripts": {
Expand Down
97 changes: 28 additions & 69 deletions packages/components/icon/src/Icon.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { css, cx } from 'emotion';
import React, {
forwardRef,
type ComponentType,
type ExoticComponent,
type ElementType,
type ReactElement,
type SVGAttributes,
} from 'react';
Expand All @@ -14,84 +13,42 @@ import {
type PolymorphicProps,
type ExpandProps,
} from '@contentful/f36-core';
import type { IconComponent, IconSize } from './types';

const ICON_DEFAULT_TAG = 'svg';

// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type IconComponent = ExoticComponent<any> | ComponentType<any>;

export type IconSize = 'xlarge' | 'large' | 'medium' | 'small' | 'tiny';

export type IconVariant =
| 'negative'
| 'positive'
| 'primary'
| 'secondary'
| 'warning'
| 'muted'
| 'white'
| 'premium';

const sizes: { [key in IconSize]: { [key in 'height' | 'width']: string } } = {
xlarge: {
height: '48px',
width: '48px',
},
large: {
height: '32px',
width: '32px',
},
medium: {
height: '24px',
width: '24px',
},
small: {
height: '18px',
width: '18px',
},
tiny: {
height: '16px',
width: '16px',
},
};

const fills: { [key in IconVariant]: string } = {
muted: tokens.gray600,
negative: tokens.red600,
positive: tokens.green600,
primary: tokens.blue600,
secondary: tokens.gray900,
warning: tokens.colorWarning,
white: tokens.colorWhite,
premium: tokens.purple500,
export const sizes: { [key in IconSize]: `${number}px` } = {
tiny: '14px',
small: '16px',
medium: '20px',
};

export type IconInternalProps = CommonProps & {
children?: ReactElement | ReactElement[];
/**
* Determines the size of the icon
* Determines the color of the icon
*/
size?: IconSize;
// @todo: We can't use the ColorTokens type here yet. Maybe fix in v5;
color?: string;
/**
* Whether or not to trim the icon width, i.e. set `width` to `auto`
* Determines the active state of the icon
*/
trimmed?: boolean;
isActive?: boolean;
/**
* Determines the fill color used
* Determines the size of the icon
*/
variant?: IconVariant;
size?: IconSize;
/**
* Custom SVG viewBox attribute to use
*/
viewBox?: SVGAttributes<SVGSVGElement>['viewBox'];
};

export type IconProps<E extends React.ElementType = IconComponent> =
PolymorphicProps<
IconInternalProps,
E,
'as' | 'children' | 'width' | 'height'
>;
export type IconProps<E extends ElementType = IconComponent> = PolymorphicProps<
IconInternalProps,
E,
'as' | 'children' | 'width' | 'height'
>;

const useAriaHidden = (
props: Pick<
Expand All @@ -111,27 +68,29 @@ const useAriaHidden = (
};
};

export function _Icon<E extends React.ElementType = IconComponent>(
export function _Icon<E extends ElementType = IconComponent>(
{
as,
children,
className,
variant = 'primary',
isActive = false,
color = isActive ? tokens.blue500 : tokens.gray900,
role = 'img',
size = 'small',
size = 'medium',
testId = 'cf-ui-icon',
trimmed,
viewBox = '0 0 24 24',
viewBox = '0 0 20 20',
...otherProps
}: IconProps<E>,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
forwardedRef: React.Ref<any>,
) {
const shared = {
className: cx(
css({
fill: fills[variant],
height: sizes[size].height,
width: trimmed ? 'auto' : sizes[size].width,
color,
fill: color,
height: sizes[size],
width: sizes[size],
}),
className,
),
Expand Down
7 changes: 3 additions & 4 deletions packages/components/icon/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
export { generateIcon } from './generateIcon';
export type { GeneratedIconProps } from './generateIcon';
export { Icon } from './Icon';
export type { IconProps, IconComponent, IconSize, IconVariant } from './Icon';
export * from './utils';
export { Icon, type IconProps } from './Icon';
export * from './types';
11 changes: 11 additions & 0 deletions packages/components/icon/src/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { type ComponentType, type ExoticComponent } from 'react';

// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type IconComponent = ExoticComponent<any> | ComponentType<any>;

export type IconSize = 'medium' | 'small' | 'tiny';

export enum IconVariant {
Active = 'active',
Default = 'default',
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { IconVariant } from '../types';
import type { GeneratedIconProps } from './generateIconComponent';

export function generateComponentWithVariants({
variants,
}: {
variants: Record<IconVariant, React.FunctionComponent<GeneratedIconProps>>;
}) {
const Component = function (props: GeneratedIconProps) {
if (props.isActive) {
return variants[IconVariant.Active](props);
}

return variants[IconVariant.Default](props);
};

return Component;
}
18 changes: 18 additions & 0 deletions packages/components/icon/src/utils/generateForma36Icon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import React from 'react';
import { IconVariant } from '../types';
import { generateComponentWithVariants } from './generateComponentWithVariants';
import { wrapPhosphorIcon } from './wrapPhosphorIcon';

/**
* Helper function to generate a Forma 36 icon component from a Phosphor icon component
*/
export function generateForma36Icon(PhosphorIcon) {
const Icon = wrapPhosphorIcon(PhosphorIcon);

return generateComponentWithVariants({
variants: {
[IconVariant.Active]: (props) => <Icon {...props} weight="fill" />,
[IconVariant.Default]: Icon,
},
});
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import React, { type ReactElement } from 'react';

import { Icon, type IconProps } from './Icon';
import { Icon, type IconProps } from '../Icon';

export type GeneratedIconProps = Omit<
IconProps,
'as' | 'children' | 'name' | 'viewBox'
> & { children?: never };
> & {
children?: never;
};

type GenerateIconParameters = {
type GenerateIconComponentParameters = {
/**
* Icon name is used as the generated icon's component display name
*/
Expand All @@ -20,26 +21,21 @@ type GenerateIconParameters = {
* A collection of default props to set on the generated icon
*/
props?: GeneratedIconProps;
/**
* Whether or not to trim the icon width, i.e. set `width` to `auto`
*/
trimmed?: IconProps['trimmed'];
/**
* Custom SVG viewBox attribute to use for the generated icon
*/
viewBox?: IconProps['viewBox'];
};

export function generateIcon({
export function generateIconComponent({
name,
path,
props: defaultProps,
trimmed,
viewBox,
}: GenerateIconParameters) {
}: GenerateIconComponentParameters) {
const Component = function (props: IconProps) {
return (
<Icon viewBox={viewBox} {...defaultProps} {...props} trimmed={trimmed}>
<Icon viewBox={viewBox} {...defaultProps} {...props}>
{path}
</Icon>
);
Expand Down
3 changes: 3 additions & 0 deletions packages/components/icon/src/utils/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from './generateComponentWithVariants';
export * from './generateForma36Icon';
export * from './generateIconComponent';
24 changes: 24 additions & 0 deletions packages/components/icon/src/utils/wrapPhosphorIcon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import React from 'react';
import type { Icon as PhosphorIcon } from '@phosphor-icons/react';
import tokens from '@contentful/f36-tokens';
import { Icon, sizes } from '../Icon';
import type { GeneratedIconProps } from './generateIconComponent';

// Unfortunately we have to pass props directly to the Phosphor icon
export function wrapPhosphorIcon(PhosphorIcon: PhosphorIcon) {
const Component = ({
isActive = false,
color = isActive ? tokens.blue500 : tokens.gray900,
size = 'medium',
...props
}: GeneratedIconProps & { weight?: 'fill' }) => {
return (
<Icon
{...props}
as={() => <PhosphorIcon {...props} color={color} size={sizes[size]} />}
/>
);
};

return Component;
}
55 changes: 1 addition & 54 deletions packages/components/icon/stories/Icon.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -68,65 +68,12 @@ export const Overview: Story = () => {
return (
<Flex marginBottom="spacingM" alignItems="center" key={size}>
<Flex marginRight="spacingS">
<CustomIcon variant="primary" size={size} />
<CustomIcon size={size} />
</Flex>
<Text>{size}</Text>
</Flex>
);
})}

<SectionHeading as="h3" marginBottom="spacingS" marginTop="spacingM">
Icon variants
</SectionHeading>

<Flex marginBottom="spacingM" alignItems="center">
<Flex marginRight="spacingS">
<CustomIcon variant="primary" />
</Flex>
<Text>primary</Text>
</Flex>

<Flex marginBottom="spacingM" alignItems="center">
<Flex marginRight="spacingS">
<CustomIcon variant="positive" />
</Flex>
<Text>positive</Text>
</Flex>

<Flex marginBottom="spacingM" alignItems="center">
<Flex marginRight="spacingS">
<CustomIcon variant="negative" />
</Flex>
<Text>negative</Text>
</Flex>

<Flex marginBottom="spacingM" alignItems="center">
<Flex marginRight="spacingS">
<CustomIcon variant="warning" />
</Flex>
<Text>warning</Text>
</Flex>

<Flex marginBottom="spacingM" alignItems="center">
<Flex marginRight="spacingS">
<CustomIcon variant="secondary" />
</Flex>
<Text>secondary</Text>
</Flex>

<Flex marginBottom="spacingM" alignItems="center">
<Flex marginRight="spacingS">
<CustomIcon variant="muted" />
</Flex>
<Text>muted</Text>
</Flex>

<Flex marginBottom="spacingM" alignItems="center">
<Flex marginRight="spacingS">
<CustomIcon variant="white" />
</Flex>
<Text>white</Text>
</Flex>
</Fragment>
);
};
1 change: 1 addition & 0 deletions packages/components/icons/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"@contentful/f36-core": "^4.48.0",
"@contentful/f36-icon": "^4.48.0",
"@contentful/f36-tokens": "^4.0.1",
"@phosphor-icons/react": "^2.0.15",
"emotion": "^10.0.17"
},
"peerDependencies": {
Expand Down
12 changes: 0 additions & 12 deletions packages/components/icons/src/Appearance.tsx

This file was deleted.

Loading

0 comments on commit 97288af

Please sign in to comment.