Improve iconography

Signed-off-by: Charles de Dreuille <charles.dedreuille@gmail.com>
This commit is contained in:
Charles de Dreuille
2024-11-28 15:18:54 +00:00
parent d0d6c54ca8
commit f64814b8e6
17 changed files with 191 additions and 107 deletions
+4
View File
@@ -2,6 +2,10 @@ import React from 'react';
import type { Preview, ReactRenderer } from '@storybook/react';
import { withThemeByDataAttribute } from '@storybook/addon-themes';
// Storybook specific styles
import '../docs/components/styles.css';
// Canon specific styles
import '../src/theme/styles.css';
const preview: Preview = {
+20
View File
@@ -0,0 +1,20 @@
import { Unstyled, Source } from '@storybook/blocks';
import { Title, Text, IconLibrary } from './components';
<Unstyled>
<Title type="h1">Iconography</Title>
<Text>
All our default icons are provided by [Remix Icon](https://remixicon.com/). We
don't import all icons to reduce the bundle size but we cherry pick a nice
selection for you to use in your application. The list of names is set down
below. To use an icon, you can use the `Icon` component and pass the name of
the icon you want to use.
</Text>
<Source code={`<Icon name="heart" />`} language="tsx" dark />
<IconLibrary />
</Unstyled>
@@ -0,0 +1,36 @@
/*
* Copyright 2024 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import { Icon } from '../../../src/components/Icon';
import type { IconNames } from '../../../src/components/Icon/types';
import { defaultIcons } from '../../../src/components/Icon/icons';
import { Text } from '../Text';
export const IconLibrary = () => {
const icons = Object.keys(defaultIcons);
return (
<div className="icon-library">
{icons.map(icon => (
<div key={icon} className="icon-library-item">
<div className="icon-library-item-icon">
<Icon name={icon as IconNames} />
</div>
<Text>{icon}</Text>
</div>
))}
</div>
);
};
@@ -0,0 +1,22 @@
.icon-library {
display: grid;
grid-template-columns: repeat(6, 1fr);
gap: 1rem;
}
.icon-library-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
}
.icon-library-item-icon {
display: flex;
width: 100%;
justify-content: center;
align-items: center;
height: 80px;
border: 1px solid #d3d3d3;
border-radius: 0.5rem;
}
+1
View File
@@ -22,3 +22,4 @@ export * from './Columns';
export * from './ComponentStatus';
export * from './LayoutComponents';
export * from './Banner';
export * from './IconLibrary';
@@ -0,0 +1 @@
@import './IconLibrary/styles.css';
+2 -2
View File
@@ -38,11 +38,11 @@
},
"dependencies": {
"@base_ui/react": "^1.0.0-alpha.3",
"@remixicon/react": "^4.5.0",
"@vanilla-extract/css": "^1.16.0",
"@vanilla-extract/dynamic": "^2.1.2",
"@vanilla-extract/recipes": "^0.5.5",
"@vanilla-extract/sprinkles": "^1.6.3",
"lucide-react": "^0.460.0"
"@vanilla-extract/sprinkles": "^1.6.3"
},
"devDependencies": {
"@backstage/cli": "workspace:^",
+10 -15
View File
@@ -9,7 +9,6 @@ import { CSSProperties } from 'react';
import { JSXElementConstructor } from 'react';
import { default as React_2 } from 'react';
import { ReactElement } from 'react';
import { ReactNode } from 'react';
// @public (undocumented)
export type AlignItems =
@@ -190,22 +189,18 @@ export type Gap = Space | Partial<Record<Breakpoint, Space>>;
export const Icon: ({ name }: { name: IconNames }) => React_2.JSX.Element;
// @public (undocumented)
export type IconNames =
| 'ArrowDown'
| 'ArrowLeft'
| 'ArrowRight'
| 'ArrowUp'
| 'Cloud'
| 'CustomIcon';
export type IconMap = Partial<Record<IconNames, React.ComponentType>>;
// @public (undocumented)
export const IconProvider: ({
children,
overrides,
}: {
children: ReactNode;
overrides: Partial<Record<IconNames, React_2.ComponentType>>;
}) => React_2.JSX.Element;
export type IconNames =
| 'arrowDown'
| 'arrowLeft'
| 'arrowRight'
| 'arrowUp'
| 'cloud'
| 'heart'
| 'plus'
| 'trash';
// @public (undocumented)
export type JustifyContent =
@@ -71,9 +71,9 @@ export const WithIcons: Story = {
},
render: args => (
<div style={{ display: 'flex', gap: '10px' }}>
<Button {...args} iconStart="Cloud" />
<Button {...args} iconEnd="ArrowRight" />
<Button {...args} iconStart="Cloud" iconEnd="ArrowRight" />
<Button {...args} iconStart="cloud" />
<Button {...args} iconEnd="arrowRight" />
<Button {...args} iconStart="cloud" iconEnd="arrowRight" />
</div>
),
};
@@ -84,9 +84,9 @@ export const FullWidth: Story = {
},
render: args => (
<Box>
<Button {...args} iconStart="Cloud" />
<Button {...args} iconEnd="ArrowRight" />
<Button {...args} iconStart="Cloud" iconEnd="ArrowRight" />
<Button {...args} iconStart="cloud" />
<Button {...args} iconEnd="arrowRight" />
<Button {...args} iconStart="cloud" iconEnd="arrowRight" />
</Box>
),
};
@@ -17,7 +17,7 @@
import React from 'react';
import { button } from './button.css';
import { Icon } from '../Icon/Icon';
import { IconNames } from '../Icon/context';
import type { IconNames } from '../Icon/types';
/**
* Properties for {@link Button}
@@ -17,8 +17,8 @@
import React from 'react';
import { Meta, StoryObj } from '@storybook/react';
import { Icon } from './Icon';
import { IconProvider } from './context';
import * as LucideIcons from 'lucide-react';
import { ThemeProvider } from '../../theme/context';
import { defaultIcons } from './icons';
const meta = {
title: 'Components/Icon',
@@ -26,15 +26,14 @@ const meta = {
parameters: {
layout: 'centered',
},
tags: ['autodocs'],
argTypes: {
name: {
control: 'select',
options: Object.keys(LucideIcons),
options: Object.keys(defaultIcons),
},
},
args: {
name: 'ArrowDown',
name: 'heart',
},
} satisfies Meta<typeof Icon>;
@@ -43,40 +42,19 @@ type Story = StoryObj<typeof meta>;
export const Primary: Story = {
args: {
name: 'ArrowDown',
},
};
export const CustomIcon: Story = {
args: {
name: 'CustomIcon',
name: 'heart',
},
};
export const WithCustomIcon: Story = {
args: {
name: 'ArrowDown',
name: 'arrowDown',
},
decorators: [
Story => (
<IconProvider overrides={{ ArrowDown: () => <div>Custom Icon</div> }}>
<ThemeProvider overrides={{ arrowDown: () => <div>Custom Icon</div> }}>
<Story />
</IconProvider>
),
],
};
export const WithCustomIconOverride: Story = {
args: {
name: 'CustomIcon',
},
decorators: [
Story => (
<IconProvider
overrides={{ CustomIcon: () => <div>Custom Super Icon</div> }}
>
<Story />
</IconProvider>
</ThemeProvider>
),
],
};
+6 -5
View File
@@ -15,18 +15,19 @@
*/
import React from 'react';
import { useIcons, IconNames } from './context';
import { useTheme } from '../../theme/context';
import type { IconNames } from './types';
/** @public */
export const Icon = ({ name }: { name: IconNames }) => {
const { icons } = useIcons();
const { icons } = useTheme();
const LucideIcon = icons[name];
const RemixIcon = icons[name];
if (!LucideIcon) {
if (!RemixIcon) {
console.error(`Icon "${name}" not found.`);
return <svg />; // Return default icon perhaps?
}
return <LucideIcon />;
return <RemixIcon />;
};
@@ -0,0 +1,41 @@
/*
* Copyright 2024 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// We can add custom icons to the list outside of Remix
import type { IconMap } from './types';
import {
RiHeartFill,
RiArrowDownFill,
RiCloudFill,
RiArrowLeftFill,
RiArrowRightFill,
RiArrowUpFill,
RiDeleteBin6Line,
RiAddLine,
} from '@remixicon/react';
// List of default icons
export const defaultIcons: IconMap = {
arrowDown: RiArrowDownFill,
arrowLeft: RiArrowLeftFill,
arrowRight: RiArrowRightFill,
arrowUp: RiArrowUpFill,
cloud: RiCloudFill,
heart: RiHeartFill,
plus: RiAddLine,
trash: RiDeleteBin6Line,
};
+1 -2
View File
@@ -15,5 +15,4 @@
*/
export * from './Icon';
export { IconProvider } from './context';
export type { IconNames } from './context';
export type * from './types';
@@ -14,10 +14,16 @@
* limitations under the License.
*/
import React from 'react';
/** @public */
export type IconNames =
| 'arrowDown'
| 'arrowLeft'
| 'arrowRight'
| 'arrowUp'
| 'cloud'
| 'heart'
| 'plus'
| 'trash';
export const CustomIcon = () => (
<svg viewBox="0 0 24 24" fill="red" width={24} height={24}>
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8z" />
</svg>
);
/** @public */
export type IconMap = Partial<Record<IconNames, React.ComponentType>>;
@@ -15,39 +15,19 @@
*/
import React, { createContext, useContext, ReactNode } from 'react';
import { ArrowUp, ArrowDown, ArrowLeft, ArrowRight, Cloud } from 'lucide-react';
import { CustomIcon } from './custom-icon';
import { IconMap, IconNames } from '../components/Icon/types';
import { defaultIcons } from '../components/Icon/icons';
// List of icons available that can also be overridden.
/** @public */
export type IconNames =
| 'ArrowDown'
| 'ArrowLeft'
| 'ArrowRight'
| 'ArrowUp'
| 'Cloud'
| 'CustomIcon';
type IconMap = Partial<Record<IconNames, React.ComponentType>>;
interface IconContextProps {
interface ThemeContextProps {
icons: IconMap;
}
// Create a default icon map with only the necessary icons
const defaultIcons: IconMap = {
ArrowDown,
ArrowLeft,
ArrowRight,
ArrowUp,
Cloud,
CustomIcon,
};
const IconContext = createContext<IconContextProps>({ icons: defaultIcons });
const ThemeContext = createContext<ThemeContextProps>({
icons: defaultIcons,
});
/** @public */
export const IconProvider = ({
export const ThemeProvider = ({
children,
overrides,
}: {
@@ -58,11 +38,11 @@ export const IconProvider = ({
const combinedIcons = { ...defaultIcons, ...overrides };
return (
<IconContext.Provider value={{ icons: combinedIcons }}>
<ThemeContext.Provider value={{ icons: combinedIcons }}>
{children}
</IconContext.Provider>
</ThemeContext.Provider>
);
};
/** @public */
export const useIcons = () => useContext(IconContext);
export const useTheme = () => useContext(ThemeContext);
+10 -10
View File
@@ -3823,6 +3823,7 @@ __metadata:
"@backstage/cli": "workspace:^"
"@base_ui/react": ^1.0.0-alpha.3
"@chromatic-com/storybook": ^3.2.2
"@remixicon/react": ^4.5.0
"@storybook/addon-essentials": ^8.4.5
"@storybook/addon-interactions": ^8.4.5
"@storybook/addon-styling-webpack": ^1.0.1
@@ -3843,7 +3844,6 @@ __metadata:
"@vanilla-extract/webpack-plugin": ^2.3.14
eslint-plugin-storybook: ^0.11.1
globals: ^15.11.0
lucide-react: ^0.460.0
mini-css-extract-plugin: ^2.9.2
react: ^18.0.2
react-dom: ^18.0.2
@@ -15458,6 +15458,15 @@ __metadata:
languageName: node
linkType: hard
"@remixicon/react@npm:^4.5.0":
version: 4.5.0
resolution: "@remixicon/react@npm:4.5.0"
peerDependencies:
react: ">=18.2.0"
checksum: e37b61090954954601d35367a740b7be30c105a49f67eaa5a697db16d4668d71d9fd94b339da6d449a254736d5af3b567d3694021b79d3c82fe28afb818830bc
languageName: node
linkType: hard
"@repeaterjs/repeater@npm:^3.0.4":
version: 3.0.5
resolution: "@repeaterjs/repeater@npm:3.0.5"
@@ -35193,15 +35202,6 @@ __metadata:
languageName: node
linkType: hard
"lucide-react@npm:^0.460.0":
version: 0.460.0
resolution: "lucide-react@npm:0.460.0"
peerDependencies:
react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc
checksum: 6106dc16dd7ce7928d6136e81c9e84e87e1b6b0910a0c78777a387c795c0512755e8bf4c602ab8f09518919999a494ed86ba7190863e8e7aec6c82665147ead3
languageName: node
linkType: hard
"lunr@npm:^2.3.9":
version: 2.3.9
resolution: "lunr@npm:2.3.9"