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 11553ba commit c9dfdf5
Show file tree
Hide file tree
Showing 198 changed files with 667 additions and 2,786 deletions.
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.56.2",
"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
114 changes: 0 additions & 114 deletions packages/components/icon/src/generateIcon.tsx

This file was deleted.

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,
},
});
}
49 changes: 49 additions & 0 deletions packages/components/icon/src/utils/generateIconComponent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import React, { type ReactElement } from 'react';
import { Icon, type IconProps } from '../Icon';

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

type GenerateIconComponentParameters = {
/**
* Icon name is used as the generated icon's component display name
*/
name?: string;
/**
* The SVG path(s) to render in the icon wrapper
*/
path: ReactElement;
/**
* A collection of default props to set on the generated icon
*/
props?: GeneratedIconProps;
/**
* Custom SVG viewBox attribute to use for the generated icon
*/
viewBox?: IconProps['viewBox'];
};

export function generateIconComponent({
name,
path,
props: defaultProps,
viewBox,
}: GenerateIconComponentParameters) {
const Component = function (props: IconProps) {
return (
<Icon viewBox={viewBox} {...defaultProps} {...props}>
{path}
</Icon>
);
};

if (name) {
Component.displayName = name;
}

return Component;
}
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';
Loading

0 comments on commit c9dfdf5

Please sign in to comment.