0
Fork 0
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:
Peter Zimon 2024-12-12 12:47:26 +02:00 committed by GitHub
parent d78802d7d1
commit 2f671eca69
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 521 additions and 165 deletions

View file

@ -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: {

View file

@ -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;
}

View file

@ -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
</>
)
}
};

View 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>
);
}
};

View 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;

View 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: ''
}
};

View 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>

View 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>

View 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>

View 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 &rarr;</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>

View 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>

View file

@ -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>

View 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);
};