mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-12-30 22:34:01 -05:00
Shade icons (#21873)
closes https://linear.app/ghost/issue/DES-1033/icon-implementation In Shade right now there's no support for icons, which is a fundamental building block in any design system. We use Streamline Icons which unfortunately don't have an out-of-the-box React support like e.g. Lucide Icons. This PR adds support for custom icons to be used directly from Shade by importing SVG's from a directory and creating React components dynamically. It also adds a grid view of all available icons in Storybook so it's easy to get an overview of available icons and copy their React component.
This commit is contained in:
parent
d78802d7d1
commit
2f671eca69
13 changed files with 521 additions and 165 deletions
|
@ -59,7 +59,7 @@ const preview: Preview = {
|
|||
options: {
|
||||
storySort: {
|
||||
method: 'alphabetical',
|
||||
order: ['Welcome', 'Foundations', ['Style Guide', 'Colors', 'Icons', 'ErrorHandling'], 'Global', ['Form', 'Chrome', 'Modal', 'Layout', ['View Container', 'Page Header', 'Page'], 'List', 'Table', '*'], 'Settings', ['Setting Section', 'Setting Group', '*'], 'Experimental'],
|
||||
order: ['Welcome', 'Adding components', 'Component usage', 'Conventions', 'Icons', 'Global', ['Form', 'Chrome', 'Modal', 'Layout', ['View Container', 'Page Header', 'Page'], 'List', 'Table', '*'], 'Settings', ['Setting Section', 'Setting Group', '*'], 'Experimental'],
|
||||
},
|
||||
},
|
||||
docs: {
|
||||
|
|
|
@ -20,7 +20,9 @@
|
|||
-webkit-text-size-adjust: 100%;
|
||||
}
|
||||
|
||||
html, body, #root {
|
||||
html,
|
||||
body,
|
||||
#root {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
|
@ -58,26 +60,32 @@ html, body, #root {
|
|||
}
|
||||
|
||||
.sb-doc a {
|
||||
color: #30CF43;
|
||||
color: #394047 !important;
|
||||
text-decoration: underline !important;
|
||||
}
|
||||
|
||||
.sb-doc a:hover {
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
.sb-doc h1 {
|
||||
font-size: 48px !important;
|
||||
font-size: 36px !important;
|
||||
letter-spacing: -0.04em !important;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.sb-doc h2 {
|
||||
margin-top: 40px !important;
|
||||
font-size: 27px;
|
||||
font-size: 24px;
|
||||
border: none;
|
||||
margin-bottom: 2px;
|
||||
letter-spacing: -0.02em !important;
|
||||
}
|
||||
|
||||
.sb-doc h3 {
|
||||
margin-top: 40px !important;
|
||||
margin-bottom: 4px !important;
|
||||
font-size: 20px;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.sb-doc h4 {
|
||||
|
@ -122,15 +130,15 @@ html, body, #root {
|
|||
.sb-doc .highlight {
|
||||
padding: 12px 20px;
|
||||
border-radius: 4px;
|
||||
background: #EBEEF0;
|
||||
background: #ebeef0;
|
||||
}
|
||||
|
||||
.sb-doc .highlight.purple {
|
||||
background: #F0E9FA;
|
||||
background: #f0e9fa;
|
||||
}
|
||||
|
||||
.sb-doc .highlight.purple a {
|
||||
color: #8E42FF;
|
||||
color: #8e42ff;
|
||||
}
|
||||
|
||||
/* Welcome */
|
||||
|
@ -166,7 +174,7 @@ html, body, #root {
|
|||
}
|
||||
|
||||
.sb-doc .main-structure-container div h4 {
|
||||
border-bottom: 1px solid #EBEEF0;
|
||||
border-bottom: 1px solid #ebeef0;
|
||||
padding-bottom: 8px !important;
|
||||
margin-bottom: 8px !important;
|
||||
}
|
||||
|
@ -191,47 +199,47 @@ html, body, #root {
|
|||
gap: 8px;
|
||||
padding: 12px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #EBEEF0;
|
||||
border: 1px solid #ebeef0;
|
||||
}
|
||||
|
||||
.color-grid .swatch {
|
||||
display: block;
|
||||
background: #EFEFEF;
|
||||
background: #efefef;
|
||||
border-radius: 100%;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
}
|
||||
|
||||
.swatch.green {
|
||||
background: #30CF43;
|
||||
background: #30cf43;
|
||||
}
|
||||
|
||||
.swatch.black {
|
||||
background: #15171A;
|
||||
background: #15171a;
|
||||
}
|
||||
|
||||
.swatch.white {
|
||||
background: #FFFFFF;
|
||||
border: 1px solid #EBEEF0;
|
||||
background: #ffffff;
|
||||
border: 1px solid #ebeef0;
|
||||
}
|
||||
|
||||
.swatch.lime {
|
||||
background: #B5FF18;
|
||||
background: #b5ff18;
|
||||
}
|
||||
.swatch.blue {
|
||||
background: #14B8FF;
|
||||
background: #14b8ff;
|
||||
}
|
||||
.swatch.purple {
|
||||
background: #8E42FF;
|
||||
background: #8e42ff;
|
||||
}
|
||||
.swatch.pink {
|
||||
background: #FB2D8D;
|
||||
background: #fb2d8d;
|
||||
}
|
||||
.swatch.yellow {
|
||||
background: #FFB41F;
|
||||
background: #ffb41f;
|
||||
}
|
||||
.swatch.red {
|
||||
background: #F50B23;
|
||||
background: #f50b23;
|
||||
}
|
||||
|
||||
/* Icons */
|
||||
|
@ -243,5 +251,70 @@ html, body, #root {
|
|||
}
|
||||
|
||||
.sbdocs-a {
|
||||
color: #30CF43 !important;
|
||||
}
|
||||
color: #394047 !important;
|
||||
text-decoration: underline !important;
|
||||
}
|
||||
|
||||
.sb-icon-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr 1fr;
|
||||
gap: 16px;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.sb-icon {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-items: center;
|
||||
gap: 4px;
|
||||
padding: 8px;
|
||||
border: 1px solid #efefef;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.sb-icon:hover {
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
|
||||
.prismjs div {
|
||||
font-size: 13px !important;
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
.docblock-source {
|
||||
border-radius: 7px !important;
|
||||
border: none !important;
|
||||
box-shadow: none !important;
|
||||
background: #15171a !important;
|
||||
}
|
||||
|
||||
.docblock-source div {
|
||||
background: unset;
|
||||
}
|
||||
|
||||
.docblock-source button {
|
||||
background: #394047 !important;
|
||||
border: none;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.sbdocs a.button {
|
||||
display: inline-block;
|
||||
padding: 6px 13px !important;
|
||||
background: #15171a;
|
||||
text-decoration: none !important;
|
||||
font-size: 13px;
|
||||
border-radius: 5px;
|
||||
color: #fff !important;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.sbdocs li > ul {
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
.sbdocs hr {
|
||||
margin: 40px 0;
|
||||
}
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import type {Meta, StoryObj} from '@storybook/react';
|
||||
|
||||
import {Button} from './button';
|
||||
import Icon from './icon';
|
||||
import {Smile} from 'lucide-react';
|
||||
|
||||
const meta = {
|
||||
title: 'Components / Button',
|
||||
|
@ -16,3 +18,33 @@ export const Default: Story = {
|
|||
children: 'This is a button component'
|
||||
}
|
||||
};
|
||||
|
||||
export const IconOnly: Story = {
|
||||
args: {
|
||||
children: (
|
||||
<Icon.ArrowUp />
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
export const IconAndText: Story = {
|
||||
args: {
|
||||
children: (
|
||||
<>
|
||||
<Icon.ArrowUp />
|
||||
Icon and text
|
||||
</>
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
export const LucideIcon: Story = {
|
||||
args: {
|
||||
children: (
|
||||
<>
|
||||
<Smile />
|
||||
Experimental
|
||||
</>
|
||||
)
|
||||
}
|
||||
};
|
52
apps/shade/src/components/ui/icon.stories.tsx
Normal file
52
apps/shade/src/components/ui/icon.stories.tsx
Normal file
|
@ -0,0 +1,52 @@
|
|||
import Icon, {IconName} from '@/components/ui/icon';
|
||||
import type {Meta, StoryObj} from '@storybook/react';
|
||||
import {useState} from 'react';
|
||||
|
||||
const meta = {
|
||||
title: 'Components / Streamline icons',
|
||||
component: Icon.Close,
|
||||
tags: ['autodocs'],
|
||||
argTypes: {
|
||||
size: {
|
||||
control: 'select',
|
||||
options: ['sm', 'md', 'lg', 'xl'],
|
||||
defaultValue: 'md'
|
||||
}
|
||||
}
|
||||
} satisfies Meta<typeof Icon.Close>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof Icon.Close>;
|
||||
|
||||
export const IconGallery = {
|
||||
render: (args: Story['args']) => {
|
||||
const icons = Object.keys(Icon) as IconName[];
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
const [copiedIcon, setCopiedIcon] = useState<string | null>(null);
|
||||
|
||||
const copyToClipboard = (iconName: string) => {
|
||||
const componentString = `<Icon.${iconName}${args?.size ? ` size="${args.size}"` : ''} />`;
|
||||
navigator.clipboard.writeText(componentString);
|
||||
setCopiedIcon(iconName);
|
||||
setTimeout(() => setCopiedIcon(null), 2000);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className='sb-icon-grid'>
|
||||
{icons.map((iconName) => {
|
||||
const IconComponent = Icon[iconName];
|
||||
return (
|
||||
<div
|
||||
key={iconName}
|
||||
className='sb-icon'
|
||||
title='Click to copy component code'
|
||||
onClick={() => copyToClipboard(iconName)}>
|
||||
<IconComponent {...args} />
|
||||
<span className="mt-2 text-sm">{copiedIcon === iconName ? 'Copied!' : iconName}</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
53
apps/shade/src/components/ui/icon.ts
Normal file
53
apps/shade/src/components/ui/icon.ts
Normal file
|
@ -0,0 +1,53 @@
|
|||
import React from 'react';
|
||||
import {cva, type VariantProps} from 'class-variance-authority';
|
||||
import {cn} from '@/lib/utils';
|
||||
import {kebabToPascalCase} from '@/utils/formatText';
|
||||
|
||||
const iconVariants = cva('', {
|
||||
variants: {
|
||||
size: {
|
||||
sm: 'h-3 w-3',
|
||||
md: 'h-4 w-4',
|
||||
lg: 'h-6 w-6',
|
||||
xl: 'h-8 w-8'
|
||||
}
|
||||
},
|
||||
defaultVariants: {
|
||||
size: 'md'
|
||||
}
|
||||
});
|
||||
|
||||
interface IconProps extends
|
||||
React.SVGProps<SVGSVGElement>,
|
||||
VariantProps<typeof iconVariants> {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const iconModules = import.meta.glob<{ReactComponent: React.FC<IconProps> }>(
|
||||
'../../assets/icons/*.svg',
|
||||
{eager: true}
|
||||
);
|
||||
|
||||
const Icon = Object.entries(iconModules).reduce((acc, [path, module]) => {
|
||||
const kebabName = path.match(/[^/]+(?=\.svg$)/)?.[0] ?? '';
|
||||
const iconName = kebabToPascalCase(kebabName);
|
||||
|
||||
const IconComponent = (props: IconProps) => {
|
||||
const {size, className, ...rest} = props;
|
||||
const iconClassName = cn(iconVariants({size, className}));
|
||||
return React.createElement(module.ReactComponent, {
|
||||
...rest,
|
||||
className: iconClassName
|
||||
});
|
||||
};
|
||||
|
||||
IconComponent.displayName = `Icon.${iconName}`;
|
||||
|
||||
acc[iconName] = IconComponent;
|
||||
return acc;
|
||||
}, {} as Record<string, React.FC<IconProps>>);
|
||||
|
||||
export type IconName = keyof typeof Icon;
|
||||
|
||||
export const IconComponents = Icon;
|
||||
export default Icon;
|
29
apps/shade/src/components/ui/lucide-icon.stories.tsx
Normal file
29
apps/shade/src/components/ui/lucide-icon.stories.tsx
Normal file
|
@ -0,0 +1,29 @@
|
|||
import type {Meta, StoryObj} from '@storybook/react';
|
||||
|
||||
import {Smile} from 'lucide-react';
|
||||
|
||||
const meta = {
|
||||
title: 'Components / Lucide icons',
|
||||
component: Smile,
|
||||
tags: ['autodocs'],
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
component: 'Note: right now we are experimenting whether we should switch to Lucide Icons instead of Streamline. Read the [Lucide Icons docs](https://lucide.dev/guide/packages/lucide-react) to learn more about it.'
|
||||
}
|
||||
}
|
||||
}
|
||||
} satisfies Meta<typeof Smile>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof Smile>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
size: 20,
|
||||
color: 'currentColor',
|
||||
strokeWidth: 2,
|
||||
absoluteStrokeWidth: false,
|
||||
className: ''
|
||||
}
|
||||
};
|
31
apps/shade/src/docs/Conventions.mdx
Normal file
31
apps/shade/src/docs/Conventions.mdx
Normal file
|
@ -0,0 +1,31 @@
|
|||
import { Meta } from '@storybook/blocks';
|
||||
|
||||
<Meta title="Conventions" />
|
||||
|
||||
<div className="sb-doc">
|
||||
|
||||
# Conventions
|
||||
|
||||
## Composable vs. configurable components
|
||||
|
||||
In Shade we favor **composable** components over configurable ones. Read about the differences and the pros and cons for each in this [article](https://blog.tomaszgil.me/choosing-the-right-path-composable-vs-configurable-components-in-react).
|
||||
|
||||
## `className` prop
|
||||
|
||||
In order to make the design system flexible, all components in Shade should have a `className` prop that is applied to the rendered HTML component. The `className` should be merged with the default classes using the `cn` util, for example:
|
||||
|
||||
```
|
||||
className={cn('bg-gray-200', className)}
|
||||
```
|
||||
|
||||
Another example if variants are used:
|
||||
|
||||
```
|
||||
className={cn(buttonVariants({variant, size, className}))}
|
||||
```
|
||||
|
||||
## TailwindCSS color naming
|
||||
|
||||
[TK]
|
||||
|
||||
</div>
|
110
apps/shade/src/docs/CreatingComponents.mdx
Normal file
110
apps/shade/src/docs/CreatingComponents.mdx
Normal file
|
@ -0,0 +1,110 @@
|
|||
import { Meta } from '@storybook/blocks';
|
||||
|
||||
<Meta title="Adding components" />
|
||||
|
||||
<div className="sb-doc">
|
||||
|
||||
# Adding new components
|
||||
|
||||
ShadCN/UI is actually not a library, it's a starting point — this means that whenever you install a new ShadCN/UI component it adds a new React `tsx` file with the default implementation for the given UI component. Then you can customize the implementation by applying TW classes, creating new variants and so on.
|
||||
|
||||
As an example, let's see how to add a "Button" component.
|
||||
|
||||
**Step 1:** Go to [ShadCN/UI component list](https://ui.shadcn.com/docs/components) and find **Button**
|
||||
|
||||
**Step 2:** Install the component
|
||||
|
||||
In the `shade` folder follow the ShadCN/UI CLI install instructions:
|
||||
|
||||
```
|
||||
npx shadcn@latest add button
|
||||
```
|
||||
|
||||
This creates a new component in Shade in `src/components/ui`:
|
||||
|
||||
```
|
||||
src/
|
||||
└── components/
|
||||
└── ui/
|
||||
└── button.tsx
|
||||
|
||||
```
|
||||
|
||||
If you take a look at the source of this component you'll see that the whole button component is directly implemented, it's not a dependency. This means a couple of things:
|
||||
|
||||
- You can't "update" ShadCN/UI components, you can only reinstall. This means that if you re-run the install command it overwrites the existing implementation including all your changes, so be careful.
|
||||
- You can change the implementation as you wish. You can add, remove or CSS classes, create new variants and add UI logic if you want.
|
||||
|
||||
**Step 3:** Create stories for the new component
|
||||
|
||||
In order to make the new component appear in Storybook, you need to create at least one story for it. You can use the Boilerplate story as a starting point: copy it as `[component].stories.tsx` to the same folder as your component:
|
||||
|
||||
```
|
||||
src/
|
||||
└── components/
|
||||
└── ui/
|
||||
├── button.tsx
|
||||
└── button.stories.tsx
|
||||
|
||||
```
|
||||
|
||||
For a story, at a minimum you'll need to:
|
||||
|
||||
- Update the imported component (for the button example: `import {Button} from './button'`), and update all references in the stories file
|
||||
- Change the meta data (most imporantly, `title: 'Components / Button'`, where "Components" will be the parent directory of Button in Storybook — this is a meta directory it doesn't have to exist in the file system)
|
||||
|
||||
**Step 4:** (Optional) Add or remove component variants
|
||||
|
||||
ShadCN/UI uses [Class Variance Authority](https://cva.style/docs) to manage variants for UI components. If you look at the Button component, you'll see an example implementation of this:
|
||||
|
||||
```
|
||||
const buttonVariants = cva(
|
||||
[default CSS classes],
|
||||
{
|
||||
variants: {
|
||||
[variant property e.g. "variant" or "size"]: {
|
||||
[variant name]: [variant CSS classes]
|
||||
}
|
||||
},
|
||||
defaultVariants: {
|
||||
[variant property e.g. "variant" or "size"]: [default value]
|
||||
}
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
Further down the code you'll see that these variants are also added to the interface of the component, which makes it appear in Storybook automatically. That's great because there's no need to create separate stories to test variants 🎉.
|
||||
|
||||
```
|
||||
export interface ButtonProps
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
VariantProps<typeof buttonVariants> { // Variants are added to the Button interface
|
||||
asChild?: boolean
|
||||
}
|
||||
```
|
||||
|
||||
That's it — you've just added a new component to Shade.
|
||||
|
||||
## Stories
|
||||
|
||||
With the automatic variant method, most of the _style related_ documentation is handled automatically in Shade. New stories should be created for non-stylistic use cases which are imporant to be able check. For the Button component an example could be related to the contents of the button: text-only, icon-only and icon-text buttons should be separated into different stories.
|
||||
|
||||
## UI component API's
|
||||
|
||||
ShadCN/UI uses [RadixUI](https://www.radix-ui.com/) under the hood which allows to use the full RadixUI API in each component. E.g. if you need a dropdown menu to be open by default you can just use the `defaultOpen` prop from [RadixUI dropdown API](https://www.radix-ui.com/primitives/docs/components/dropdown-menu).
|
||||
|
||||
For [Charts](https://ui.shadcn.com/charts), ShadCN/UI uses [Recharts](https://recharts.org/en-US/). Similarly to RadixUI, you can use the Recharts API to customize and use charting features.
|
||||
|
||||
## ShadCN/UI configuration
|
||||
|
||||
The `components.json` file contains the ShadCN/UI default configuration, so when you install a new component these options are taken into consideration. There are some of them worth mentioning:
|
||||
|
||||
- `"style": "new-york"` — We chose "New York" because visually it's closer to Ghost's UI style than the "Default" style
|
||||
- `"baseColor": "gray"` — This means that all newly installed ShadCN/UI component will use the color code `gray` (with an `a`, not `grey`) to set default colors
|
||||
- `"cssVariables": true` — This ensures to use CSS variable names instead of colors as a default in new components which means less more consistency and less code. However if a component doesn't work with the default colors, there's always an option to override colors in the component implementation.
|
||||
|
||||
## Custom components
|
||||
|
||||
[TK]
|
||||
|
||||
</div>
|
40
apps/shade/src/docs/Environment.mdx
Normal file
40
apps/shade/src/docs/Environment.mdx
Normal file
|
@ -0,0 +1,40 @@
|
|||
import { Meta } from '@storybook/blocks';
|
||||
|
||||
<Meta title="Code editor setup" />
|
||||
|
||||
<div className="sb-doc">
|
||||
|
||||
# VSCode/Cursor setup
|
||||
|
||||
In order to make the proper TailwindCSS classes appear in code completion, you need to make sure that your code editor is set up correctly. First, make sure you have the Tailwind CSS IntelliSense extension installed:
|
||||
|
||||
1. Open VSCode extensions
|
||||
2. Search for "Tailwind CSS IntelliSense" and install it
|
||||
|
||||
Then, verify your VSCode settings:
|
||||
|
||||
1. Open settings.json (Cmd+Shift+P, then "Preferences: Open Settings (JSON)")
|
||||
2. Add these settings:
|
||||
|
||||
```
|
||||
{
|
||||
"tailwindCSS.includeLanguages": {
|
||||
"typescript": "javascript",
|
||||
"typescriptreact": "javascript"
|
||||
},
|
||||
"tailwindCSS.experimental.configFile": "tailwind.config.cjs"
|
||||
}
|
||||
```
|
||||
|
||||
Also, ensure your project has a postcss.config.js file:
|
||||
|
||||
```
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
</div>
|
42
apps/shade/src/docs/Icons.mdx
Normal file
42
apps/shade/src/docs/Icons.mdx
Normal file
|
@ -0,0 +1,42 @@
|
|||
import { Meta } from '@storybook/blocks';
|
||||
|
||||
<Meta title="Icons" />
|
||||
|
||||
<div className="sb-doc">
|
||||
|
||||
# Icons
|
||||
|
||||
In Shade we use Streamline icons. They are dynamically loaded from the `src/assets/icons` library, so to add a new icon you just need to create the corresponding SVG for it in this folder. Then you can use the Icon component like this:
|
||||
|
||||
```
|
||||
<Icon.IconName size="md" />
|
||||
```
|
||||
|
||||
If you check out the [Icons](/docs/components-streamline-icons--docs) component here in Storybook, you'll see a grid of all available icons in Shade. This list is dynamically loaded and if you click on an icon then the component code will be copied to your clipboard.
|
||||
|
||||
<a href="/?path=/docs/components-streamline-icons--docs" className="button">Streamline icons in Shade →</a>
|
||||
|
||||
|
||||
## Adding icons
|
||||
|
||||
We need to make sure we stay consistent when adding new icons, so here are some rules to follow when doing so:
|
||||
|
||||
- Copy icons from the [Streamline Ultimate Regular collection](https://www.streamlinehq.com/icons/streamline-regular)
|
||||
- SVG properties in Streamline should be:
|
||||
- Size: 24px
|
||||
- Stroke width: 1.5px
|
||||
- Outline stroke: OFF
|
||||
- Responsive size: OFF
|
||||
- Currentcolor: ON
|
||||
- Create the icon in `src/assets/icons`. The icon name should be in kebab case, ie. all lowercase, words separated by dash (-). Examples:
|
||||
- ✅ `layout-column-3.svg`
|
||||
- ❌ `LayoutColumn3.svg`
|
||||
|
||||
|
||||
---
|
||||
|
||||
## Experimental: Lucide Icons
|
||||
|
||||
ShadCN/UI uses Lucide Icons by default. Right now we're experimenting with using it instead of Streamline icons but haven't decided to go with it. To learn how to use them, read the [Lucide Icons docs](https://lucide.dev/guide/packages/lucide-react).
|
||||
|
||||
</div>
|
25
apps/shade/src/docs/UsingComponents.mdx
Normal file
25
apps/shade/src/docs/UsingComponents.mdx
Normal file
|
@ -0,0 +1,25 @@
|
|||
import { Meta } from '@storybook/blocks';
|
||||
|
||||
<Meta title="Component usage" />
|
||||
|
||||
<div className="sb-doc">
|
||||
|
||||
# Component usage
|
||||
|
||||
The simplest way to use a component is to select the variant in Storybook and copy + paste the code from it. All components have their `className` prop forwarded to their rendered HTML component, which allows you to override any default classes.
|
||||
|
||||
## UI component API's
|
||||
|
||||
ShadCN/UI uses [RadixUI](https://www.radix-ui.com/) under the hood which allows to use the full RadixUI API in each component. E.g. if you need a dropdown menu to be open by default you can just use the `defaultOpen` prop from [RadixUI dropdown API](https://www.radix-ui.com/primitives/docs/components/dropdown-menu).
|
||||
|
||||
For [Charts](https://ui.shadcn.com/charts), ShadCN/UI uses [Recharts](https://recharts.org/en-US/). Similarly to RadixUI, you can use the Recharts API to customize and use charting features.
|
||||
|
||||
## ShadCN/UI configuration
|
||||
|
||||
The `components.json` file contains the ShadCN/UI default configuration, so when you install a new component these options are taken into consideration. There are some of them worth mentioning:
|
||||
|
||||
- `"style": "new-york"` — We chose "New York" because visually it's closer to Ghost's UI style than the "Default" style
|
||||
- `"baseColor": "gray"` — This means that all newly installed ShadCN/UI component will use the color code `gray` (with an `a`, not `grey`) to set default colors
|
||||
- `"cssVariables": true` — This ensures to use CSS variable names instead of colors as a default in new components which means less more consistency and less code. However if a component doesn't work with the default colors, there's always an option to override colors in the component implementation.
|
||||
|
||||
</div>
|
|
@ -1,153 +1,16 @@
|
|||
import { Meta } from '@storybook/blocks';
|
||||
|
||||
<Meta title="About" />
|
||||
<Meta title="Welcome" />
|
||||
|
||||
<div className="sb-doc">
|
||||
|
||||
# Shade
|
||||
|
||||
**Shade** is Ghost's design system for product design. It contains all components and their usage which help you design a great product experience.
|
||||
<p className="excerpt">**Shade** is Ghost's design system for product design. It contains all the necessary components and their usage that help you design a great product experience.</p>
|
||||
|
||||
## Stack
|
||||
|
||||
Shade uses the RadixUI based open source UI library, [ShadCN/UI](https://ui.shadcn.com/) as its foundation which does the heavy lifting and gives a great starting point for any UI component. Additionally Shade is built using [TailwindCSS](https://www.tailwindcss.com). Before diving deep into working with Shade, please get familiar with both libraries.
|
||||
|
||||
As you're reading this, Shade also uses [Storybook](https://storybook.js.org/) to document components and best practices.
|
||||
|
||||
## Creating a new component
|
||||
|
||||
ShadCN/UI is not a dependency, it's a starting point — this means that whenever you install a new ShadCN/UI component it adds a new React `tsx` file with the default implementation for the given UI component. Then you can customize the implementation by applying TW classes, creating new variants and so on.
|
||||
|
||||
As an example, let's see how to add a "Button" component.
|
||||
|
||||
**Step 1:** Go to [ShadCN/UI component list](https://ui.shadcn.com/docs/components) and find **Button**
|
||||
|
||||
**Step 2:** Install the component
|
||||
|
||||
In the `shade` folder follow the CLI install instructions:
|
||||
|
||||
```
|
||||
npx shadcn@latest add button
|
||||
```
|
||||
|
||||
This creates a new component in Shade in `src/components/ui`:
|
||||
|
||||
```
|
||||
src/
|
||||
└── components/
|
||||
└── ui/
|
||||
└── button.tsx
|
||||
|
||||
```
|
||||
|
||||
If you take a look at the source of this component you'll see that the whole button component is directly implemented, it's not a dependency. This means a couple of things:
|
||||
|
||||
- You can't "update" ShadCN/UI components, you can only reinstall. This means that if you re-run the install command it overwrites the existing implementation including all your changes, so be careful.
|
||||
- You can change the implementation as you wish. You can add, remove or CSS classes, create new variants and add UI logic if you want.
|
||||
|
||||
**Step 3:** Create stories for the new component
|
||||
|
||||
In order to make the new component appear in Storybook, you need to create at least one story for it. You can use the Boilerplate story as a starting point: copy it as `[component].stories.tsx` to the same folder as your component:
|
||||
|
||||
```
|
||||
src/
|
||||
└── components/
|
||||
└── ui/
|
||||
├── button.tsx
|
||||
└── button.stories.tsx
|
||||
|
||||
```
|
||||
|
||||
For a story, at a minimum you'll need to:
|
||||
|
||||
- Update the imported component (for the button example: `import {Button} from './button'`), and update all references in the stories file
|
||||
- Change the meta data (most imporantly, `title: 'Components / Button'`, where "Components" will be the parent directory of Button in Storybook — this is a meta directory it doesn't have to exist in the file system)
|
||||
|
||||
**Step 4:** (Optional) Add or remove component variants
|
||||
|
||||
ShadCN/UI uses [Class Variance Authority](https://cva.style/docs) to manage variants for UI components. If you look at the Button component, you'll see an example implementation of this:
|
||||
|
||||
```
|
||||
const buttonVariants = cva(
|
||||
[default CSS classes],
|
||||
{
|
||||
variants: {
|
||||
[variant property e.g. "variant" or "size"]: {
|
||||
[variant name]: [variant CSS classes]
|
||||
}
|
||||
},
|
||||
defaultVariants: {
|
||||
[variant property e.g. "variant" or "size"]: [default value]
|
||||
}
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
Further down the code you'll see that these variants are also added to the interface of the component, which makes it appear in Storybook automatically. That's great because there's no need to create separate stories to test variants 🎉.
|
||||
|
||||
```
|
||||
export interface ButtonProps
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
VariantProps<typeof buttonVariants> { // Variants are added to the Button interface
|
||||
asChild?: boolean
|
||||
}
|
||||
```
|
||||
|
||||
That's it — you've just added a new component to Shade.
|
||||
|
||||
## UI component API's
|
||||
|
||||
ShadCN/UI uses [RadixUI](https://www.radix-ui.com/) under the hood which allows to use the full RadixUI API in each component. E.g. if you need a dropdown menu to be open by default you can just use the `defaultOpen` prop from [RadixUI dropdown API](https://www.radix-ui.com/primitives/docs/components/dropdown-menu).
|
||||
|
||||
For [Charts](https://ui.shadcn.com/charts), ShadCN/UI uses [Recharts](https://recharts.org/en-US/). Similarly to RadixUI, you can use the Recharts API to customize and use charting features.
|
||||
|
||||
## ShadCN/UI configuration
|
||||
|
||||
The `components.json` file contains the ShadCN/UI default configuration, so when you install a new component these options are taken into consideration. There are some of them worth mentioning:
|
||||
|
||||
- `"style": "new-york"` — We chose "New York" because visually it's closer to Ghost's UI style than the "Default" style
|
||||
- `"baseColor": "gray"` — This means that all newly installed ShadCN/UI component will use the color code `gray` (with an `a`, not `grey`) to set default colors
|
||||
- `"cssVariables": true` — This ensures to use CSS variable names instead of colors as a default in new components which means less more consistency and less code. However if a component doesn't work with the default colors, there's always an option to override colors in the component implementation.
|
||||
|
||||
## Component usage
|
||||
|
||||
The simplest way to use a component is to select the variant in Storybook and copy + paste the code from it. All components have their `className` prop forwarded to their rendered HTML component, which allows you to override any default classes.
|
||||
|
||||
|
||||
## Stories
|
||||
|
||||
With the above automatic variant method, most of the _style related_ documentation is handled automatically in Shade. New stories should be created for non-stylistic use cases which are imporant to be able check. For the Button component an example could be related to the contents of the button: text-only, icon-only and icon-text buttons should be separated into different stories.
|
||||
|
||||
## Custom components
|
||||
|
||||
[TK]
|
||||
|
||||
## Icons
|
||||
|
||||
[TK]
|
||||
|
||||
## Coding conventions
|
||||
|
||||
**Composable vs. configurable components**
|
||||
|
||||
In Shade we favor **composable** components over configurable ones. Read about the differences and the pros and cons for each in this [article](https://blog.tomaszgil.me/choosing-the-right-path-composable-vs-configurable-components-in-react).
|
||||
|
||||
**`className` prop**
|
||||
|
||||
In order to make the design system flexible, all components in Shade should have a `className` prop that is applied to the rendered HTML component. The `className` should be merged with the default classes using the `cn` util, for example:
|
||||
|
||||
```
|
||||
className={cn('bg-gray-200', className)}
|
||||
```
|
||||
|
||||
Or another example if variants are used:
|
||||
|
||||
```
|
||||
className={cn(buttonVariants({variant, size, className}))}
|
||||
```
|
||||
|
||||
**TailwindCSS color naming**
|
||||
|
||||
We use TailwindCSS's standard color naming, including `gray` (_not_ `grey`). The main reason for this
|
||||
Shade uses the RadixUI based open source UI library, [ShadCN/UI](https://ui.shadcn.com/) as its foundation which does the heavy lifting and gives a great starting point for any UI component. Additionally Shade is built using [TailwindCSS](https://www.tailwindcss.com). Before diving deep into working with Shade, please get familiar with both of these libraries.
|
||||
|
||||
Shade uses [Storybook](https://storybook.js.org/) to document components and best practices.
|
||||
</div>
|
||||
|
|
6
apps/shade/src/utils/formatText.ts
Normal file
6
apps/shade/src/utils/formatText.ts
Normal file
|
@ -0,0 +1,6 @@
|
|||
// Helper to convert kebab-case to PascalCase with numbers
|
||||
export const kebabToPascalCase = (str: string): string => {
|
||||
const processed = str
|
||||
.replace(/[-_]([a-z0-9])/gi, (_, char) => char.toUpperCase());
|
||||
return processed.charAt(0).toUpperCase() + processed.slice(1);
|
||||
};
|
Loading…
Reference in a new issue