mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-01-20 22:42:53 -05:00
Added dynamic icon component in AdminX
refs. https://github.com/TryGhost/Team/issues/3150
This commit is contained in:
parent
5c20aa08de
commit
0a02768ec1
6 changed files with 203 additions and 14 deletions
|
@ -0,0 +1,60 @@
|
|||
import type {Meta, StoryObj} from '@storybook/react';
|
||||
|
||||
import Icon from './Icon';
|
||||
|
||||
const meta = {
|
||||
title: 'Global / Icon',
|
||||
component: Icon,
|
||||
tags: ['autodocs']
|
||||
} satisfies Meta<typeof Icon>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof Icon>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
name: 'lock-locked'
|
||||
}
|
||||
};
|
||||
|
||||
export const ExtraSmall: Story = {
|
||||
args: {
|
||||
size: 'xs',
|
||||
name: 'lock-locked'
|
||||
}
|
||||
};
|
||||
|
||||
export const Small: Story = {
|
||||
args: {
|
||||
size: 'sm',
|
||||
name: 'lock-locked'
|
||||
}
|
||||
};
|
||||
|
||||
export const Medium: Story = {
|
||||
args: {
|
||||
size: 'md',
|
||||
name: 'lock-locked'
|
||||
}
|
||||
};
|
||||
|
||||
export const Large: Story = {
|
||||
args: {
|
||||
size: 'lg',
|
||||
name: 'lock-locked'
|
||||
}
|
||||
};
|
||||
|
||||
export const ExtraLarge: Story = {
|
||||
args: {
|
||||
size: 'xl',
|
||||
name: 'lock-locked'
|
||||
}
|
||||
};
|
||||
|
||||
export const Color: Story = {
|
||||
args: {
|
||||
color: 'green',
|
||||
name: 'lock-locked'
|
||||
}
|
||||
};
|
93
ghost/admin-x-settings/src/admin-x-ds/global/Icon.tsx
Normal file
93
ghost/admin-x-settings/src/admin-x-ds/global/Icon.tsx
Normal file
|
@ -0,0 +1,93 @@
|
|||
import React, {useEffect, useRef, useState} from 'react';
|
||||
|
||||
interface UseDynamicSVGImportOptions {
|
||||
onCompleted?: (
|
||||
name: string,
|
||||
SvgIcon: React.FC<React.SVGProps<SVGSVGElement>> | undefined
|
||||
) => void;
|
||||
onError?: (err: Error) => void;
|
||||
}
|
||||
|
||||
function useDynamicSVGImport(
|
||||
name: string,
|
||||
options: UseDynamicSVGImportOptions = {}
|
||||
) {
|
||||
const ImportedIconRef = useRef<React.FC<React.SVGProps<SVGSVGElement>>>();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<Error>();
|
||||
|
||||
const {onCompleted, onError} = options;
|
||||
useEffect(() => {
|
||||
setLoading(true);
|
||||
const importIcon = async (): Promise<void> => {
|
||||
try {
|
||||
ImportedIconRef.current = (
|
||||
await import(`../../assets/icons/${name}.svg`)
|
||||
).ReactComponent;
|
||||
onCompleted?.(name, ImportedIconRef.current);
|
||||
} catch (err: any) {
|
||||
onError?.(err);
|
||||
setError(err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
importIcon();
|
||||
}, [name, onCompleted, onError]);
|
||||
|
||||
return {error, loading, SvgIcon: ImportedIconRef.current};
|
||||
}
|
||||
|
||||
export type IconSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl' | number;
|
||||
|
||||
interface IconProps {
|
||||
name: string;
|
||||
|
||||
/**
|
||||
* Accepts either predefined sizes or number, in which case the size means the pixel width & height
|
||||
*/
|
||||
size?: IconSize;
|
||||
color?: string;
|
||||
styles?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const Icon: React.FC<IconProps> = ({name, size = 'md', color = 'black', className}) => {
|
||||
const {SvgIcon} = useDynamicSVGImport(name);
|
||||
|
||||
let styles = '';
|
||||
|
||||
if (!styles) {
|
||||
switch (size) {
|
||||
case 'xs':
|
||||
styles = 'w-3 h-3';
|
||||
break;
|
||||
case 'sm':
|
||||
styles = 'w-4 h-4';
|
||||
break;
|
||||
case 'lg':
|
||||
styles = 'w-8 h-8';
|
||||
break;
|
||||
case 'xl':
|
||||
styles = 'w-10 h-10';
|
||||
break;
|
||||
|
||||
default:
|
||||
styles = 'w-5 h-5';
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (color) {
|
||||
styles += ` text-${color}`;
|
||||
}
|
||||
|
||||
if (SvgIcon) {
|
||||
return (
|
||||
<SvgIcon className={`${styles} ${className}`} />
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
export default Icon;
|
|
@ -0,0 +1,20 @@
|
|||
import type {Meta, StoryObj} from '@storybook/react';
|
||||
|
||||
import IconLabel from './IconLabel';
|
||||
|
||||
const meta = {
|
||||
title: 'Global / Label with icon',
|
||||
component: IconLabel,
|
||||
tags: ['autodocs']
|
||||
} satisfies Meta<typeof IconLabel>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof IconLabel>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
icon: 'check-circle',
|
||||
iconColor: 'green',
|
||||
children: 'Here\'s a label with icon'
|
||||
}
|
||||
};
|
19
ghost/admin-x-settings/src/admin-x-ds/global/IconLabel.tsx
Normal file
19
ghost/admin-x-settings/src/admin-x-ds/global/IconLabel.tsx
Normal file
|
@ -0,0 +1,19 @@
|
|||
import Icon from './Icon';
|
||||
import React from 'react';
|
||||
|
||||
interface IconLabelProps {
|
||||
icon: string;
|
||||
iconColor?: string;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
const IconLabel: React.FC<IconLabelProps> = ({icon, iconColor, children}) => {
|
||||
return (
|
||||
<div className='flex items-center gap-2'>
|
||||
<Icon color={iconColor} name={icon} size='sm' />
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default IconLabel;
|
|
@ -1,11 +1,11 @@
|
|||
import Dropdown from '../../../admin-x-ds/global/Dropdown';
|
||||
import IconLabel from '../../../admin-x-ds/global/IconLabel';
|
||||
import Link from '../../../admin-x-ds/global/Link';
|
||||
import React from 'react';
|
||||
import SettingGroup from '../../../admin-x-ds/settings/SettingGroup';
|
||||
import SettingGroupContent from '../../../admin-x-ds/settings/SettingGroupContent';
|
||||
import TextField from '../../../admin-x-ds/global/TextField';
|
||||
import useSettingGroup from '../../../hooks/useSettingGroup';
|
||||
import {ReactComponent as CheckIcon} from '../../../assets/icons/check-circle.svg';
|
||||
|
||||
const MAILGUN_REGIONS = [
|
||||
{label: '🇺🇸 US', value: 'https://api.mailgun.net/v3'},
|
||||
|
@ -32,9 +32,9 @@ const MailGun: React.FC = () => {
|
|||
{
|
||||
key: 'status',
|
||||
value: (
|
||||
<div className='flex items-center'>
|
||||
<CheckIcon className='mr-2 h-4 w-4 text-green' /> Mailgun is set up
|
||||
</div>
|
||||
<IconLabel icon='check-circle' iconColor='green'>
|
||||
Mailgun is set up
|
||||
</IconLabel>
|
||||
)
|
||||
}
|
||||
] : [
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import IconLabel from '../../../admin-x-ds/global/IconLabel';
|
||||
import Link from '../../../admin-x-ds/global/Link';
|
||||
import React from 'react';
|
||||
import SettingGroup from '../../../admin-x-ds/settings/SettingGroup';
|
||||
|
@ -5,8 +6,6 @@ import SettingGroupContent from '../../../admin-x-ds/settings/SettingGroupConten
|
|||
import TextField from '../../../admin-x-ds/global/TextField';
|
||||
import Toggle from '../../../admin-x-ds/global/Toggle';
|
||||
import useSettingGroup from '../../../hooks/useSettingGroup';
|
||||
import {ReactComponent as LockedIcon} from '../../../assets/icons/lock-locked.svg';
|
||||
import {ReactComponent as UnLockedIcon} from '../../../assets/icons/lock-unlocked.svg';
|
||||
|
||||
const LockSite: React.FC = () => {
|
||||
const {
|
||||
|
@ -34,15 +33,13 @@ const LockSite: React.FC = () => {
|
|||
{
|
||||
key: 'private',
|
||||
value: passwordEnabled ? (
|
||||
<div className='flex items-center '>
|
||||
<LockedIcon className='mr-2 h-4 w-4 text-yellow' />
|
||||
<span>Your site is password protected</span>
|
||||
</div>
|
||||
<IconLabel icon='lock-locked' iconColor='yellow'>
|
||||
Your site is password protected
|
||||
</IconLabel>
|
||||
) : (
|
||||
<div className='flex items-center text-grey-900 '>
|
||||
<UnLockedIcon className='mr-2 h-4 w-4' />
|
||||
<span>Your site is not password protected</span>
|
||||
</div>
|
||||
<IconLabel icon='lock-unlocked' iconColor='grey-900'>
|
||||
Your site is password protected
|
||||
</IconLabel>
|
||||
)
|
||||
}
|
||||
]}
|
||||
|
|
Loading…
Add table
Reference in a new issue