Skip to content
Merged
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
8 changes: 4 additions & 4 deletions src/components/Examples/ExamplesFilter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@ import React, { ChangeEvent, Dispatch, SetStateAction, useCallback, useEffect, u
import ReactDOM from 'react-dom';
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';
import { navigate } from 'gatsby';
import { ProductName } from '@ably/ui/core/ProductTile/data';
import { MagnifyingGlassIcon, XMarkIcon } from '@heroicons/react/24/outline';
import { AdjustmentsHorizontalIcon, MagnifyingGlassIcon, XMarkIcon } from '@heroicons/react/24/outline';

// Matches Tailwind's `sm` screen (768px), where the filter switches from the
// mobile drawer (with an Apply button) to the inline desktop sidebar. Above
Expand Down Expand Up @@ -135,7 +135,7 @@ const ExamplesFilter = ({
<Button
className="flex sm:hidden mt-4 w-full"
variant="secondary"
leftIcon="icon-gui-adjustments-horizontal-outline"
leftIcon={<AdjustmentsHorizontalIcon aria-hidden />}
onClick={() => setExpandFilterMenu(true)}
>
Filter
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 @@ -3,3 +3,5 @@ import { glyphs } from './glyphs';
// Static glyph names are checked against the vendored registry; `icon-tech-${language}`
// stays open for the dynamically-built language icons.
export type IconName = keyof typeof glyphs | `icon-tech-${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, { ComponentType, SVGProps, useEffect, useMemo, useState } from 're
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';
import { HandRaisedIcon, HandThumbDownIcon, HandThumbUpIcon } from '@heroicons/react/24/outline';
import {
HandRaisedIcon as HandRaisedSolidIcon,
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 LeftSidebar from './LeftSidebar';
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, ReactNode, useMemo } from 'react';
import { IconSize } from 'src/components/Icon/types';
import cn from 'src/utilities/cn';
import { ColorClassColorGroups } from './colors';
import IconSlot from './IconSlot';

/**
* 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';

/**
* An icon element to display before the children. Pass it unsized — the badge sizes it.
*/
iconBefore?: ReactNode;

/**
* An icon element to display after the children. Pass it unsized — the badge sizes it.
*/
iconAfter?: ReactNode;

/**
* 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 ? <IconSlot icon={iconBefore} size={computedIconSize} colorClass={colorClass} /> : null}

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

{iconAfter ? <IconSlot icon={iconAfter} size={computedIconSize} colorClass={colorClass} /> : null}
</div>
);
};

export default Badge;
Loading
Loading