Improve iconography
Signed-off-by: Charles de Dreuille <charles.dedreuille@gmail.com>
This commit is contained in:
@@ -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 = {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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';
|
||||
@@ -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:^",
|
||||
|
||||
@@ -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>
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
@@ -15,5 +15,4 @@
|
||||
*/
|
||||
|
||||
export * from './Icon';
|
||||
export { IconProvider } from './context';
|
||||
export type { IconNames } from './context';
|
||||
export type * from './types';
|
||||
|
||||
+12
-6
@@ -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>>;
|
||||
+10
-30
@@ -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);
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user