Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@
"react-medium-image-zoom": "^5.4.1",
"react-select": "^5.7.0",
"remark-gfm": "^1.0.0",
"swr": "^2.4.0",
"tailwind-merge": "^2.5.5",
"typescript": "^4.6.3",
"use-keyboard-shortcut": "^1.1.6",
Expand Down
4 changes: 2 additions & 2 deletions src/components/Examples/ExamplesFilter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ import ReactDOM from 'react-dom';
import Icon from 'src/components/Icon';
import { Input } from 'src/components/ui/Input';
import { products } from '../../data/examples';
import Button from '@ably/ui/core/Button';
import Button from 'src/components/ui/Button';
import cn from 'src/utilities/cn';
import Badge from '@ably/ui/core/Badge';
import Badge from 'src/components/ui/Badge';
import ExamplesCheckbox from './ExamplesCheckbox';
import { SelectedFilters } from './ExamplesContent';
import { useOnClickOutside } from 'src/hooks/use-on-click-outside';
Expand Down
2 changes: 1 addition & 1 deletion src/components/Examples/ExamplesGrid.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React, { useCallback } from 'react';
import Badge from '@ably/ui/core/Badge';
import Badge from 'src/components/ui/Badge';
import Icon from 'src/components/Icon';
import { IconName } from 'src/components/Icon/types';
import { ProductName, products as dataProducts } from '@ably/ui/core/ProductTile/data';
Expand Down
2 changes: 1 addition & 1 deletion src/components/Examples/ExamplesNoResults.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import Badge from '@ably/ui/core/Badge';
import Badge from 'src/components/ui/Badge';
import { Link } from 'gatsby';

const ExamplesNoResults = () => {
Expand Down
4 changes: 2 additions & 2 deletions src/components/Homepage/Changelog.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { useStaticQuery } from 'gatsby';
import { graphql } from 'gatsby';
import { Key } from 'react';
import Badge from '@ably/ui/core/Badge';
import FeaturedLink from '@ably/ui/core/FeaturedLink';
import Badge from 'src/components/ui/Badge';
import FeaturedLink from 'src/components/ui/FeaturedLink';
import Link from '../Link';

interface ChangelogFeedItemNode {
Expand Down
2 changes: 1 addition & 1 deletion src/components/Homepage/ExamplesSection.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { ExamplesSectionData } from 'src/data/content/types';
import { getImageFromList, ImageProps } from 'src/components/Image';
import { Image } from 'src/components/Image';
import FeaturedLink from '@ably/ui/core/FeaturedLink';
import FeaturedLink from 'src/components/ui/FeaturedLink';

export const ExamplesSection = ({ section, images }: { section: ExamplesSectionData; images: ImageProps[] }) => {
const imageUrl = getImageFromList(images, section.image);
Expand Down
2 changes: 2 additions & 0 deletions src/components/Icon/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,5 @@
// static `icon-gui-*` / `icon-social-*` / `icon-product-*` names and the
// dynamic `icon-tech-${language}` usages, while keeping the icon-* convention.
export type IconName = `icon-${string}`;

export type IconSize = `${number}px` | `${number}em` | `${number}rem` | `calc(${string})`;
4 changes: 2 additions & 2 deletions src/components/Layout/Footer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@ import React, { useEffect, useMemo, useState } from 'react';
import { useLocation } from '@reach/router';
import Icon from 'src/components/Icon';
import { IconName } from 'src/components/Icon/types';
import Status, { StatusUrl } from '@ably/ui/core/Status';
import Status, { StatusUrl } from 'src/components/ui/Status';
import cn from 'src/utilities/cn';
import type { PageContextType } from './Layout';
import { useLayoutContext } from 'src/contexts/layout-context';
import Button from '@ably/ui/core/Button';
import Button from 'src/components/ui/Button';

const ENABLE_FEEDBACK = false;

Expand Down
2 changes: 1 addition & 1 deletion src/components/Layout/Header.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ jest.mock('src/components/Icon', () => {
return MockIcon;
});

jest.mock('@ably/ui/core/LinkButton', () => {
jest.mock('src/components/ui/LinkButton', () => {
const MockButton: React.FC<{ children: React.ReactNode }> = ({ children }) => <button>{children}</button>;
MockButton.displayName = 'MockButton';
return MockButton;
Expand Down
2 changes: 1 addition & 1 deletion src/components/Layout/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { throttle } from 'es-toolkit/compat';
import cn from 'src/utilities/cn';
import Icon from 'src/components/Icon';
import TabMenu from '@ably/ui/core/TabMenu';
import Logo from '@ably/ui/core/images/logo/ably-logo.svg';
import Logo from 'src/images/ably-logo.svg';
import { track } from '@ably/ui/core/insights';
import { componentMaxHeight, HEADER_BOTTOM_MARGIN, HEADER_HEIGHT } from 'src/utilities/heights';
import { IconName } from 'src/components/Icon/types';
Expand Down
2 changes: 1 addition & 1 deletion src/components/Layout/LanguageSelector.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ jest.mock('src/components/Icon', () => ({
default: ({ name }: { name: string }) => <div>{name}</div>,
}));

jest.mock('@ably/ui/core/Badge', () => ({
jest.mock('src/components/ui/Badge', () => ({
__esModule: true,
default: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
}));
Expand Down
2 changes: 1 addition & 1 deletion src/components/Layout/LanguageSelector.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useEffect, useMemo, useState } from 'react';
import { useLocation } from '@reach/router';
import Badge from '@ably/ui/core/Badge';
import Badge from 'src/components/ui/Badge';
import Icon from 'src/components/Icon';
import { IconName } from 'src/components/Icon/types';
import cn from 'src/utilities/cn';
Expand Down
2 changes: 1 addition & 1 deletion src/components/Layout/mdx/RequiredBadge.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React from 'react';
import Badge from '@ably/ui/core/Badge';
import Badge from 'src/components/ui/Badge';

// The wrapping div with data-toc-exclude prevents this badge's text from
// appearing in the "On this page" sidebar when rendered inside a heading.
Expand Down
164 changes: 164 additions & 0 deletions src/components/ui/Badge.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
import React, { PropsWithChildren, useMemo } from 'react';
import { IconName, IconSize } from 'src/components/Icon/types';
import Icon from 'src/components/Icon';
import cn from 'src/utilities/cn';
import { ColorClassColorGroups } from './colors';

/**
* Props for the Badge component.
*/
export interface BadgeProps {
/**
* The size of the badge. Can be one of "xs", "sm", "md", or "lg".
*/
size?: 'xs' | 'sm' | 'md' | 'lg';

/**
* The color of the badge. Can be a value from ColorClassColorGroups or "red".
*/
color?: ColorClassColorGroups | 'red';

/**
* The name of the icon to be displayed before the children in the badge.
*/
iconBefore?: IconName;

/**
* The name of the icon to be displayed after the children in the badge.
*/
iconAfter?: IconName;

/**
* Additional CSS class names to apply to the badge.
*/
className?: string;

/**
* Whether the badge is disabled. Defaults to false.
*/
disabled?: boolean;

/**
* Whether the badge is focusable. Defaults to false.
*/
focusable?: boolean;

/**
* Whether the badge is hoverable. Defaults to false.
*/
hoverable?: boolean;

/**
* The size of the icons in the badge. If not provided, it will be derived from the badge size.
*/
iconSize?: IconSize;

/**
* Accessible label for the badge when interactive
*/
ariaLabel?: string;

/**
* Additional CSS class names to apply to the children of the badge.
*/
childClassName?: string;
}

const defaultIconSizeByBadgeSize: Record<NonNullable<BadgeProps['size']>, IconSize> = {
lg: '16px',
md: '15px',
sm: '14px',
xs: '13px',
};

const Badge: React.FC<PropsWithChildren<BadgeProps>> = ({
size = 'md',
color = 'neutral',
iconBefore,
iconAfter,
className,
childClassName,
children,
disabled = false,
focusable = false,
hoverable = false,
iconSize,
ariaLabel,
}) => {
const sizeClass = useMemo(() => {
switch (size) {
case 'xs':
return 'px-2 py-0 text-[10px] leading-tight';
case 'sm':
return 'px-2 py-0.5 text-[10px] leading-tight';
case 'md':
return 'px-2.5 py-0.5 text-[11px] leading-normal';
case 'lg':
return 'px-3 py-[0.1875rem] text-[12px] leading-normal';
}
}, [size]);

const childClass = useMemo(() => {
switch (size) {
case 'xs':
case 'sm':
return 'leading-[18px]';
case 'md':
case 'lg':
return 'leading-[20px]';
}
}, [size]);

const colorClass = useMemo(() => {
switch (color) {
case 'neutral':
return 'text-neutral-900 dark:text-neutral-400';
case 'violet':
return 'text-violet-400';
case 'orange':
return 'text-orange-600';
case 'yellow':
return 'text-yellow-600';
case 'green':
return 'text-green-600';
case 'blue':
return 'text-blue-600';
case 'pink':
return 'text-pink-600';
case 'red':
return 'text-orange-700';
}
}, [color]);

const computedIconSize = iconSize ?? defaultIconSizeByBadgeSize[size];

return (
<div
className={cn(
'inline-flex bg-neutral-100 dark:bg-neutral-1200 rounded-2xl gap-1 items-center focus-base transition-colors select-none font-semibold',
sizeClass,
colorClass,
{ 'focus-base': focusable },
{
'hover:bg-neutral-300 hover:dark:bg-neutral-1000 active:bg-neutral-300 dark:active:bg-neutral-1000':
hoverable,
},
{
'cursor-not-allowed disabled:text-gui-disabled-light dark:disabled:text-gui-disabled-dark': disabled,
},
className,
)}
tabIndex={focusable ? 0 : undefined}
role={focusable ? 'button' : undefined}
aria-label={focusable || hoverable ? ariaLabel : undefined}
>
{iconBefore ? <Icon name={iconBefore} size={computedIconSize} color={colorClass} /> : null}

<span className={cn('whitespace-nowrap tracking-[0.04em]', childClass, childClassName)}>{children}</span>

{iconAfter ? <Icon name={iconAfter} size={computedIconSize} color={colorClass} /> : null}
</div>
);
};

export default Badge;
113 changes: 113 additions & 0 deletions src/components/ui/Button.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import React, { PropsWithChildren } from 'react';
import { IconName } from 'src/components/Icon/types';
import Icon from 'src/components/Icon';
import cn from 'src/utilities/cn';
import { ColorClass, ColorThemeSet } from './colors';

export type ButtonType = 'priority' | 'primary' | 'secondary';

type ButtonSize = 'lg' | 'md' | 'sm' | 'xs';

export type ButtonPropsBase = {
/**
* The type of button: priority, primary, or secondary.
*/
variant?: ButtonType;
/**
* The button size: lg, sm, or xs. Leave empty for md.
*/
size?: ButtonSize;
/**
* An icon to render on the left side of the button label.
*/
leftIcon?: IconName;
/**
* An icon to render on the right side of the button label.
*/
rightIcon?: IconName;
/**
* Optional classes to add to the button element.
*/
className?: string;
/**
* Optional color to apply to the icon on either left and/or right side of the button.
*/
iconColor?: ColorClass | ColorThemeSet;
};

type ButtonProps = ButtonPropsBase & React.ButtonHTMLAttributes<HTMLButtonElement>;

// got to go the long way round because of ol' mate Taily Waily
const buttonClasses: Record<ButtonType, Record<ButtonSize, string>> = {
priority: {
lg: 'ui-button-priority-lg',
md: 'ui-button-priority',
sm: 'ui-button-priority-sm',
xs: 'ui-button-priority-xs',
},
primary: {
lg: 'ui-button-primary-lg',
md: 'ui-button-primary',
sm: 'ui-button-primary-sm',
xs: 'ui-button-primary-xs',
},
secondary: {
lg: 'ui-button-secondary-lg',
md: 'ui-button-secondary',
sm: 'ui-button-secondary-sm',
xs: 'ui-button-secondary-xs',
},
};

export const iconModifierClasses: Record<ButtonSize, { left: string; right: string }> = {
lg: { left: 'ui-button-lg-left-icon', right: 'ui-button-lg-right-icon' },
md: { left: 'ui-button-left-icon', right: 'ui-button-right-icon' },
sm: { left: 'ui-button-sm-left-icon', right: 'ui-button-sm-right-icon' },
xs: { left: '', right: '' },
};

export const commonButtonProps = (props: ButtonPropsBase) => {
const { variant = 'primary', size, leftIcon, rightIcon, className } = props;

return {
className: cn(
buttonClasses[variant][size ?? 'md'],
{ [iconModifierClasses[size ?? 'md'].left]: leftIcon },
{ [iconModifierClasses[size ?? 'md'].right]: rightIcon },
className,
),
};
};

export const commonButtonInterior = (props: PropsWithChildren<ButtonPropsBase>) => {
const { leftIcon, rightIcon, iconColor, children } = props;
return (
<>
{leftIcon ? <Icon name={leftIcon} additionalCSS={iconColor} /> : null}
{children}
{rightIcon ? <Icon name={rightIcon} additionalCSS={iconColor} /> : null}
</>
);
};

const Button: React.FC<PropsWithChildren<ButtonProps>> = ({
variant = 'primary',
size,
leftIcon,
rightIcon,
children,
className,
iconColor,
...rest
}) => {
return (
<button
{...commonButtonProps({ variant, size, leftIcon, rightIcon, className })}
{...(rest as React.ButtonHTMLAttributes<HTMLButtonElement>)}
>
{commonButtonInterior({ leftIcon, rightIcon, iconColor, children })}
</button>
);
};

export default Button;
Loading