diff --git a/package.json b/package.json
index 7289a338e1..392c8af0c6 100644
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/src/components/Examples/ExamplesFilter.tsx b/src/components/Examples/ExamplesFilter.tsx
index 49710d4d22..0c2dca12c3 100644
--- a/src/components/Examples/ExamplesFilter.tsx
+++ b/src/components/Examples/ExamplesFilter.tsx
@@ -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';
diff --git a/src/components/Examples/ExamplesGrid.tsx b/src/components/Examples/ExamplesGrid.tsx
index 6f1ab43e1e..285925a667 100644
--- a/src/components/Examples/ExamplesGrid.tsx
+++ b/src/components/Examples/ExamplesGrid.tsx
@@ -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';
diff --git a/src/components/Examples/ExamplesNoResults.tsx b/src/components/Examples/ExamplesNoResults.tsx
index ddab95d64c..b8d23c10e1 100644
--- a/src/components/Examples/ExamplesNoResults.tsx
+++ b/src/components/Examples/ExamplesNoResults.tsx
@@ -1,4 +1,4 @@
-import Badge from '@ably/ui/core/Badge';
+import Badge from 'src/components/ui/Badge';
import { Link } from 'gatsby';
const ExamplesNoResults = () => {
diff --git a/src/components/Homepage/Changelog.tsx b/src/components/Homepage/Changelog.tsx
index b98f44282b..f98b95950e 100644
--- a/src/components/Homepage/Changelog.tsx
+++ b/src/components/Homepage/Changelog.tsx
@@ -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 {
diff --git a/src/components/Homepage/ExamplesSection.tsx b/src/components/Homepage/ExamplesSection.tsx
index 6caae9c4a2..dd2995ccc5 100644
--- a/src/components/Homepage/ExamplesSection.tsx
+++ b/src/components/Homepage/ExamplesSection.tsx
@@ -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);
diff --git a/src/components/Icon/types.ts b/src/components/Icon/types.ts
index ef3cef4426..135e75ef27 100644
--- a/src/components/Icon/types.ts
+++ b/src/components/Icon/types.ts
@@ -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})`;
diff --git a/src/components/Layout/Footer.tsx b/src/components/Layout/Footer.tsx
index 917421b363..b4118175f5 100644
--- a/src/components/Layout/Footer.tsx
+++ b/src/components/Layout/Footer.tsx
@@ -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;
diff --git a/src/components/Layout/Header.test.tsx b/src/components/Layout/Header.test.tsx
index 70d760e110..b022a18249 100644
--- a/src/components/Layout/Header.test.tsx
+++ b/src/components/Layout/Header.test.tsx
@@ -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 }) => ;
MockButton.displayName = 'MockButton';
return MockButton;
diff --git a/src/components/Layout/Header.tsx b/src/components/Layout/Header.tsx
index abfb9e1530..dc5bd2a6b9 100644
--- a/src/components/Layout/Header.tsx
+++ b/src/components/Layout/Header.tsx
@@ -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';
diff --git a/src/components/Layout/LanguageSelector.test.tsx b/src/components/Layout/LanguageSelector.test.tsx
index 1f5e86eb4e..af1e136602 100644
--- a/src/components/Layout/LanguageSelector.test.tsx
+++ b/src/components/Layout/LanguageSelector.test.tsx
@@ -14,7 +14,7 @@ jest.mock('src/components/Icon', () => ({
default: ({ name }: { name: string }) =>
{name}
,
}));
-jest.mock('@ably/ui/core/Badge', () => ({
+jest.mock('src/components/ui/Badge', () => ({
__esModule: true,
default: ({ children }: { children: React.ReactNode }) => {children}
,
}));
diff --git a/src/components/Layout/LanguageSelector.tsx b/src/components/Layout/LanguageSelector.tsx
index f1dd7bc118..d166d21d13 100644
--- a/src/components/Layout/LanguageSelector.tsx
+++ b/src/components/Layout/LanguageSelector.tsx
@@ -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';
diff --git a/src/components/Layout/mdx/RequiredBadge.tsx b/src/components/Layout/mdx/RequiredBadge.tsx
index 3770452d9f..7a28fab4db 100644
--- a/src/components/Layout/mdx/RequiredBadge.tsx
+++ b/src/components/Layout/mdx/RequiredBadge.tsx
@@ -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.
diff --git a/src/components/ui/Badge.tsx b/src/components/ui/Badge.tsx
new file mode 100644
index 0000000000..70ab0cd5c9
--- /dev/null
+++ b/src/components/ui/Badge.tsx
@@ -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, IconSize> = {
+ lg: '16px',
+ md: '15px',
+ sm: '14px',
+ xs: '13px',
+};
+
+const Badge: React.FC> = ({
+ 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 (
+
+ {iconBefore ? : null}
+
+ {children}
+
+ {iconAfter ? : null}
+
+ );
+};
+
+export default Badge;
diff --git a/src/components/ui/Button.tsx b/src/components/ui/Button.tsx
new file mode 100644
index 0000000000..b6cdaf9433
--- /dev/null
+++ b/src/components/ui/Button.tsx
@@ -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;
+
+// got to go the long way round because of ol' mate Taily Waily
+const buttonClasses: Record> = {
+ 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 = {
+ 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) => {
+ const { leftIcon, rightIcon, iconColor, children } = props;
+ return (
+ <>
+ {leftIcon ? : null}
+ {children}
+ {rightIcon ? : null}
+ >
+ );
+};
+
+const Button: React.FC> = ({
+ variant = 'primary',
+ size,
+ leftIcon,
+ rightIcon,
+ children,
+ className,
+ iconColor,
+ ...rest
+}) => {
+ return (
+
+ );
+};
+
+export default Button;
diff --git a/src/components/ui/FeaturedLink.tsx b/src/components/ui/FeaturedLink.tsx
new file mode 100644
index 0000000000..7f27aa588e
--- /dev/null
+++ b/src/components/ui/FeaturedLink.tsx
@@ -0,0 +1,130 @@
+import React, { CSSProperties, ReactNode } from 'react';
+
+import Icon from 'src/components/Icon';
+import { ColorClass, ColorThemeSet } from './colors';
+import cn from 'src/utilities/cn';
+
+type FeaturedLinkProps = {
+ url: string;
+ children: ReactNode;
+ textSize?: string;
+ iconColor?: ColorClass | ColorThemeSet;
+ flush?: boolean;
+ reverse?: boolean;
+ additionalCSS?: string;
+ newWindow?: boolean;
+ onClick?: () => void;
+ disabled?: boolean;
+ /**
+ * Optional class name for the icon.
+ */
+ iconClassName?: string;
+};
+
+type TargetProps = { target?: string; rel?: string };
+
+// When generating links with target=_blank, we only add `noreferrer` to
+// links that don't start with `/`, so we can continue tracking referrers
+// across our own domains
+const buildTargetAndRel = (url: string, newWindow: boolean) => {
+ const props: TargetProps = {};
+
+ if (newWindow) {
+ props.target = '_blank';
+
+ if (url.startsWith('/') && !url.startsWith('//')) {
+ props.rel = 'noopener';
+ } else {
+ props.rel = 'noopenner noreferrer';
+ }
+ }
+
+ return props;
+};
+
+const FeaturedLink = ({
+ url,
+ textSize = 'text-p2',
+ iconColor,
+ flush = false,
+ reverse = false,
+ additionalCSS = '',
+ newWindow = false,
+ onClick = undefined,
+ children,
+ disabled = false,
+ iconClassName = '',
+}: FeaturedLinkProps) => {
+ const targetAndRel = buildTargetAndRel(url, newWindow);
+
+ return (
+ {
+ if (e.key === 'Enter' || e.key === ' ') {
+ e.preventDefault();
+ onClick();
+ }
+ }
+ : undefined
+ }
+ role={onClick && !url ? 'button' : undefined}
+ >
+ {reverse ? (
+ <>
+
+ {children}
+ >
+ ) : (
+ <>
+ {children}
+
+ >
+ )}
+
+ );
+};
+
+export default FeaturedLink;
diff --git a/src/components/ui/LinkButton.tsx b/src/components/ui/LinkButton.tsx
new file mode 100644
index 0000000000..12983b788f
--- /dev/null
+++ b/src/components/ui/LinkButton.tsx
@@ -0,0 +1,83 @@
+import React from 'react';
+import { ButtonPropsBase, commonButtonInterior, commonButtonProps } from './Button';
+import cn from 'src/utilities/cn';
+import { ColorClass, ColorThemeSet } from './colors';
+
+export type LinkButtonProps = ButtonPropsBase & {
+ disabled?: boolean;
+ onClick?: (event: React.MouseEvent) => void;
+ iconColor?: ColorClass | ColorThemeSet;
+} & React.AnchorHTMLAttributes;
+
+const LinkButton: React.FC = ({
+ variant = 'primary',
+ size,
+ leftIcon,
+ rightIcon,
+ children,
+ className,
+ disabled,
+ onClick,
+ iconColor,
+ ...rest
+}) => {
+ const handleClick = (e: React.MouseEvent) => {
+ if (disabled) {
+ e.preventDefault();
+ return;
+ }
+ onClick?.(e);
+ };
+
+ const handleKeyDown = (e: React.KeyboardEvent) => {
+ if (e.repeat) {
+ return;
+ }
+ // Space: prevent page scroll on keydown; activate on keyup.
+ if (e.key === ' ' || e.key === 'Spacebar') {
+ e.preventDefault();
+ return;
+ }
+ // Enter activates on keydown.
+ if (e.key === 'Enter') {
+ e.preventDefault();
+ if (!disabled) {
+ e.currentTarget.click();
+ }
+ }
+ };
+
+ const handleKeyUp = (e: React.KeyboardEvent) => {
+ if (e.key === ' ' || e.key === 'Spacebar') {
+ e.preventDefault();
+ if (!disabled) {
+ e.currentTarget.click();
+ }
+ }
+ };
+
+ return (
+ )}
+ >
+ {commonButtonInterior({ leftIcon, rightIcon, iconColor, children })}
+
+ );
+};
+
+export default LinkButton;
diff --git a/src/components/ui/Status.tsx b/src/components/ui/Status.tsx
new file mode 100644
index 0000000000..fd8870b430
--- /dev/null
+++ b/src/components/ui/Status.tsx
@@ -0,0 +1,82 @@
+import React from 'react';
+import useSWR from 'swr';
+import cn from 'src/utilities/cn';
+import Icon from 'src/components/Icon';
+
+type StatusProps = {
+ statusUrl: string;
+ additionalCSS?: string;
+ refreshInterval?: number;
+ showDescription?: boolean;
+};
+
+export const statusTypes = ['none', 'operational', 'minor', 'major', 'critical', 'unknown'] as const;
+
+export type StatusType = (typeof statusTypes)[number];
+
+export const StatusUrl = 'https://ntqy1wz94gjv.statuspage.io/api/v2/status.json';
+
+// Our SWR fetcher function
+const fetcher = (url: string) => fetch(url).then((res) => res.json());
+
+const indicatorClass = (indicator?: StatusType) => {
+ switch (indicator) {
+ case 'none':
+ case 'operational':
+ return 'bg-gui-success-green';
+ case 'minor':
+ return 'bg-yellow-500';
+ case 'major':
+ return 'bg-orange-500';
+ case 'critical':
+ return 'bg-gui-error-red';
+ default:
+ return 'bg-neutral-500';
+ }
+};
+
+export const StatusIcon = ({ statusUrl, refreshInterval = 1000 * 60 }: StatusProps) => {
+ const { data, error, isLoading } = useSWR(statusUrl, fetcher, {
+ refreshInterval,
+ });
+
+ return (
+
+ );
+};
+
+const Status = ({
+ statusUrl = StatusUrl,
+ additionalCSS,
+ refreshInterval = 1000 * 60,
+ showDescription = false,
+}: StatusProps) => {
+ const { data } = useSWR(statusUrl, fetcher, {
+ refreshInterval,
+ });
+
+ return (
+
+
+ {showDescription && data?.status?.description && (
+
+
+ {data.status.description.charAt(0).toUpperCase() + data.status.description.slice(1).toLowerCase()}
+
+
+
+ )}
+
+ );
+};
+
+export default Status;
diff --git a/src/components/ui/colors.ts b/src/components/ui/colors.ts
new file mode 100644
index 0000000000..693bff7431
--- /dev/null
+++ b/src/components/ui/colors.ts
@@ -0,0 +1,182 @@
+export type ColorName =
+ | (typeof neutralColors)[number]
+ | (typeof orangeColors)[number]
+ | (typeof secondaryColors)[number]
+ | (typeof guiColors)[number]
+ | (typeof aliasedColors)[number];
+
+export const variants = ['', 'dark:'] as const;
+
+type ColorClassVariants = (typeof variants)[number];
+
+export const prefixes = ['text', 'bg', 'from', 'to', 'border'] as const;
+
+type ColorClassPrefixes = (typeof prefixes)[number];
+
+export const colors = ['neutral', 'orange', 'blue', 'yellow', 'green', 'violet', 'pink'] as const;
+
+export type ColorClassColorGroups = (typeof colors)[number];
+
+export type Theme = 'light' | 'dark';
+
+export type ColorClass = `${ColorClassVariants}${ColorClassPrefixes}-${ColorName}`;
+
+export type ColorThemeSet = `${string} dark:${string}`;
+
+export const neutralColors = [
+ 'neutral-000',
+ 'neutral-100',
+ 'neutral-200',
+ 'neutral-300',
+ 'neutral-400',
+ 'neutral-500',
+ 'neutral-600',
+ 'neutral-700',
+ 'neutral-800',
+ 'neutral-900',
+ 'neutral-1000',
+ 'neutral-1100',
+ 'neutral-1200',
+ 'neutral-1300',
+] as const;
+
+export const orangeColors = [
+ 'orange-100',
+ 'orange-200',
+ 'orange-300',
+ 'orange-400',
+ 'orange-500',
+ 'orange-600',
+ 'orange-700',
+ 'orange-800',
+ 'orange-900',
+ 'orange-1000',
+ 'orange-1100',
+] as const;
+
+export const yellowColors = [
+ 'yellow-100',
+ 'yellow-200',
+ 'yellow-300',
+ 'yellow-400',
+ 'yellow-500',
+ 'yellow-600',
+ 'yellow-700',
+ 'yellow-800',
+ 'yellow-900',
+] as const;
+
+export const greenColors = [
+ 'green-100',
+ 'green-200',
+ 'green-300',
+ 'green-400',
+ 'green-500',
+ 'green-600',
+ 'green-700',
+ 'green-800',
+ 'green-900',
+] as const;
+
+export const blueColors = [
+ 'blue-100',
+ 'blue-200',
+ 'blue-300',
+ 'blue-400',
+ 'blue-500',
+ 'blue-600',
+ 'blue-700',
+ 'blue-800',
+ 'blue-900',
+] as const;
+
+export const violetColors = [
+ 'violet-100',
+ 'violet-200',
+ 'violet-300',
+ 'violet-400',
+ 'violet-500',
+ 'violet-600',
+ 'violet-700',
+ 'violet-800',
+ 'violet-900',
+] as const;
+
+export const pinkColors = [
+ 'pink-100',
+ 'pink-200',
+ 'pink-300',
+ 'pink-400',
+ 'pink-500',
+ 'pink-600',
+ 'pink-700',
+ 'pink-800',
+ 'pink-900',
+] as const;
+
+export const secondaryColors = [
+ ...yellowColors,
+ ...greenColors,
+ ...blueColors,
+ ...violetColors,
+ ...pinkColors,
+] as const;
+
+export const guiColors = [
+ 'gui-blue-default-light',
+ 'gui-blue-hover-light',
+ 'gui-blue-active-light',
+ 'gui-blue-default-dark',
+ 'gui-blue-hover-dark',
+ 'gui-blue-active-dark',
+ 'gui-blue-focus',
+ 'gui-disabled-light',
+ 'gui-disabled-dark',
+ 'gui-success-green',
+ 'gui-error-red',
+ 'gui-focus',
+ 'gui-focus-outline',
+ 'gui-visited',
+] as const;
+
+export const aliasedColors = [
+ 'white',
+ 'extra-light-grey',
+ 'light-grey',
+ 'mid-grey',
+ 'dark-grey',
+ 'charcoal-grey',
+ 'cool-black',
+ 'active-orange',
+ 'bright-red',
+ 'red-orange',
+ 'electric-cyan',
+ 'zingy-green',
+ 'jazzy-pink',
+ 'gui-default',
+ 'gui-hover',
+ 'gui-active',
+ 'gui-error',
+ 'gui-success',
+ 'gui-default-dark',
+ 'gui-hover-dark',
+ 'gui-active-dark',
+ 'transparent',
+] as const;
+
+export const colorRoles = {
+ neutral: neutralColors,
+ orange: orangeColors,
+ secondary: secondaryColors,
+ gui: guiColors,
+};
+
+export const colorGroupLengths = {
+ neutral: neutralColors.length,
+ orange: orangeColors.length,
+ blue: blueColors.length,
+ yellow: yellowColors.length,
+ green: greenColors.length,
+ violet: violetColors.length,
+ pink: pinkColors.length,
+};
diff --git a/src/images/ably-logo.svg b/src/images/ably-logo.svg
new file mode 100644
index 0000000000..b8b2895f0e
--- /dev/null
+++ b/src/images/ably-logo.svg
@@ -0,0 +1,23 @@
+
diff --git a/src/templates/examples.tsx b/src/templates/examples.tsx
index 9fb99d1d3c..d5bcd9a58d 100644
--- a/src/templates/examples.tsx
+++ b/src/templates/examples.tsx
@@ -2,7 +2,7 @@ import React, { PropsWithChildren, useContext, useEffect, useRef } from 'react';
import Markdown from 'markdown-to-jsx/react';
import { Link } from 'gatsby';
import Icon from 'src/components/Icon';
-import LinkButton from '@ably/ui/core/LinkButton';
+import LinkButton from 'src/components/ui/LinkButton';
import { UnstyledOpenInCodeSandboxButton } from '@codesandbox/sandpack-react';
import { Head } from '../components/Head';
diff --git a/yarn.lock b/yarn.lock
index acf0862fb2..f6443f8210 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -15187,6 +15187,14 @@ swr@^2.2.5:
dequal "^2.0.3"
use-sync-external-store "^1.6.0"
+swr@^2.4.0:
+ version "2.4.1"
+ resolved "https://registry.yarnpkg.com/swr/-/swr-2.4.1.tgz#c9e48abff6bf4b04846342e2f1f6be108a078cf6"
+ integrity sha512-2CC6CiKQtEwaEeNiqWTAw9PGykW8SR5zZX8MZk6TeAvEAnVS7Visz8WzphqgtQ8v2xz/4Q5K+j+SeMaKXeeQIA==
+ dependencies:
+ dequal "^2.0.3"
+ use-sync-external-store "^1.6.0"
+
symbol-tree@^3.2.4:
version "3.2.4"
resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2"