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
9 changes: 6 additions & 3 deletions gatsby-browser.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { PostHogProvider } from 'posthog-js/react';
import posthog from 'posthog-js';

import UserContextWrapper from './src/contexts/user-context/wrap-with-provider';
import { ThemeProvider } from './src/contexts/theme-context';
import { useSiteMetadata } from './src/hooks/use-site-metadata';

const onClientEntry: GatsbyBrowser['onClientEntry'] = () => {
Expand Down Expand Up @@ -56,9 +57,11 @@ const InsightsWrapper = ({ children }) => {

export const wrapRootElement = ({ element }) => {
return (
<InsightsWrapper>
<UserContextWrapper element={element} />
</InsightsWrapper>
<ThemeProvider>
<InsightsWrapper>
<UserContextWrapper element={element} />
</InsightsWrapper>
</ThemeProvider>
);
};

Expand Down
16 changes: 14 additions & 2 deletions gatsby-ssr.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
import React from 'react';
import type { GatsbySSR } from 'gatsby';
import { getSandpackCssText } from '@codesandbox/sandpack-react';
import { THEME_NO_FLASH_SCRIPT } from './src/utilities/theme';

const onRenderBody: GatsbySSR['onRenderBody'] = ({ setHeadComponents }) => {
const inlineScripts: React.ReactNode[] = [];

// Set the theme class on <html> before first paint to avoid a flash of the
// wrong theme. Runs first so the class is present before any styles apply.
inlineScripts.push(<script key="theme-no-flash" dangerouslySetInnerHTML={{ __html: THEME_NO_FLASH_SCRIPT }} />);

// OneTrust consent management, inspiration taken from gatsby-google-tagmanager implementation
if (process.env.ONE_TRUST_ENABLED === 'true' && !!process.env.ONE_TRUST_DOMAIN) {
let domainId = process.env.ONE_TRUST_DOMAIN;
Expand Down Expand Up @@ -117,9 +122,16 @@ const onPreRenderHTML: GatsbySSR['onPreRenderHTML'] = ({ getHeadComponents, repl
};

/**
* Load our user state
* Load our user state, wrapped in the theme provider so useTheme is available
* everywhere (including pages that don't use the docs Layout).
*/
import UserContextWrapper from './src/contexts/user-context/wrap-with-provider';
const wrapRootElement = UserContextWrapper;
import { ThemeProvider } from './src/contexts/theme-context';

const wrapRootElement: GatsbySSR['wrapRootElement'] = ({ element }) => (
<ThemeProvider>
<UserContextWrapper element={element} />
</ThemeProvider>
);

export { onRenderBody, onPreRenderHTML, wrapRootElement };
4 changes: 2 additions & 2 deletions src/components/Examples/ExamplesContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -80,15 +80,15 @@ const ExamplesContent = ({ exampleImages }: { exampleImages: ImageProps[] }) =>
width={660}
height={282}
alt="Grid Pattern"
className="!absolute -z-10 right-0 top-16 !hidden sm:!block w-[60%] md:w-[40%]"
className="!absolute -z-10 right-0 top-16 !hidden sm:!block dark:!hidden w-[60%] md:w-[40%]"
/>

<StaticImage
src="./images/mobile-grid.png"
placeholder="blurred"
width={260}
alt="Grid Pattern"
className="-z-10 right-0 top-16 !absolute !block sm:!hidden"
className="-z-10 right-0 top-16 !absolute !block sm:!hidden dark:!hidden"
/>
</>
);
Expand Down
10 changes: 5 additions & 5 deletions src/components/Examples/ExamplesGrid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ const ExamplesGrid = ({

return text.split(searchRegex).map((part, index) =>
part.toLowerCase() === searchTerm.toLowerCase() ? (
<span key={index} className="bg-yellow-200">
<span key={index} className="bg-yellow-200 dark:bg-yellow-800">
{part}
</span>
) : (
Expand All @@ -86,19 +86,19 @@ const ExamplesGrid = ({
role="button"
aria-label={`View ${name} example`}
>
<div className="z-0 bg-neutral-100 overflow-hidden h-64 sm:h-[12.5rem] relative flex justify-center items-center ">
<div className="z-0 bg-neutral-100 dark:bg-neutral-1200 overflow-hidden h-64 sm:h-[12.5rem] relative flex justify-center items-center ">
<div className="group-hover/examples-index-card:scale-105 transition-transform">
{exampleImages ? displayExampleImage(exampleImages, id, name) : null}
</div>
<div className="flex bg-neutral-000 gap-x-1.5 py-1.5 px-2 absolute right-3 bottom-3 rounded border border-neutral-200 z-10">
<div className="flex bg-neutral-000 dark:bg-neutral-1300 gap-x-1.5 py-1.5 px-2 absolute right-3 bottom-3 rounded border border-neutral-200 dark:border-neutral-1000 z-10">
{(languages ?? DEFAULT_EXAMPLE_LANGUAGES).map((language) => (
<Icon key={language} name={`icon-tech-${language}` as IconName} size="18px" />
))}
</div>
</div>
<div className="z-10 pt-4">
<p className="ui-text-h4 text-neutral-1300">{highlightSearchTerm(name)}</p>
<p className="ui-text-p3 mt-2 text-neutral-900">{highlightSearchTerm(description)}</p>
<p className="ui-text-h4 text-neutral-1300 dark:text-neutral-000">{highlightSearchTerm(name)}</p>
<p className="ui-text-p3 mt-2 text-neutral-900 dark:text-neutral-500">{highlightSearchTerm(description)}</p>
<div className="mt-4 flex gap-x-1">
{products ? products.map((product) => displayProductLabel(product as ProductName, dataProducts)) : null}
{/* {useCases ? useCases.map((useCase) => displayUseCaseLabel(useCase)) : null} */}
Expand Down
17 changes: 13 additions & 4 deletions src/components/Examples/ExamplesRenderer.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import React, { PropsWithChildren, useMemo } from 'react';
import React, { PropsWithChildren, useEffect, useMemo, useState } from 'react';
import { SandpackProvider, SandpackPreview } from '@codesandbox/sandpack-react';
import { githubLight } from '@codesandbox/sandpack-themes';
import { CodeEditor } from 'src/components/CodeEditor';
import { CodeEditor, sandpackTheme } from 'src/components/CodeEditor';
import { useTheme } from 'src/contexts/theme-context';
import { LanguageKey } from 'src/data/languages/types';
import { ExampleFiles, ExampleWithContent } from 'src/data/examples/types';
import { updateAblyConnectionKey } from 'src/utilities/update-ably-connection-keys';
Expand Down Expand Up @@ -74,6 +75,14 @@ const ExamplesRenderer = ({
children,
}: PropsWithChildren<ExamplesRendererProps>) => {
const { id, files, visibleFiles, layout, products } = example;
const { resolvedTheme } = useTheme();
// Sandpack/Chrome take the theme as JS values (not CSS `dark:` variants), so
// gate on mount: render light (matching the server) until hydrated, then
// follow the resolved theme. Avoids a hydration mismatch for dark-mode users
// when the editor is server-rendered.
const [mounted, setMounted] = useState(false);
useEffect(() => setMounted(true), []);
const editorTheme = mounted ? resolvedTheme : 'light';

const rewrittenFiles = useMemo<ExampleFiles>(() => {
const result: ExampleFiles = {};
Expand Down Expand Up @@ -149,7 +158,7 @@ const ExamplesRenderer = ({
: []),
],
}}
theme={githubLight}
theme={editorTheme === 'dark' ? sandpackTheme : githubLight}
template={determineSandpackTemplate(activeLanguage as LanguageKey)}
>
<div
Expand Down Expand Up @@ -181,7 +190,7 @@ const ExamplesRenderer = ({
>
<div className={cn('min-w-0 overflow-hidden', isVerticalLayout && 'md:flex-1')}>
<CodeEditor
theme="light"
theme={editorTheme}
editor={{
className: '',
showLineNumbers: true,
Expand Down
4 changes: 2 additions & 2 deletions src/components/Homepage/ExamplesSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ export const ExamplesSection = ({ section, images }: { section: ExamplesSectionD
const backgroundGrid = getImageFromList(images, 'examples_grid.png');

return (
<div className="relative overflow-hidden flex flex-col rounded-lg border border-neutral-300 dark:border-neutral-1000 pt-6 pl-6 md:p-6 lg:p-8 bg-gradient-to-b from-[#fff] to-[#F6F8FA]">
<div className="relative overflow-hidden flex flex-col rounded-lg border border-neutral-300 dark:border-neutral-1000 pt-6 pl-6 md:p-6 lg:p-8 bg-gradient-to-b from-[#fff] to-[#F6F8FA] dark:bg-none">
{backgroundGrid && (
<div className="w-full h-full absolute top-0 right-0 z-0">
<div className="w-full h-full absolute top-0 right-0 z-0 dark:hidden">
<Image image={backgroundGrid} />
</div>
)}
Expand Down
4 changes: 2 additions & 2 deletions src/components/Homepage/HomepageContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,14 @@ export const HomepageContent = ({
{mobileImage && (
<Image
image={mobileImage}
className="absolute top-[3.9375rem] right-0 z-[-1] block sm:hidden w-auto h-auto pointer-events-none"
className="absolute top-[3.9375rem] right-0 z-[-1] block sm:hidden dark:hidden w-auto h-auto pointer-events-none"
aria-hidden="true"
/>
)}
{desktopImage && (
<Image
image={desktopImage}
className="absolute top-[3.9375rem] right-0 z-[-1] hidden sm:block w-auto h-auto pointer-events-none"
className="absolute top-[3.9375rem] right-0 z-[-1] hidden sm:block dark:hidden w-auto h-auto pointer-events-none"
aria-hidden="true"
/>
)}
Expand Down
14 changes: 9 additions & 5 deletions src/components/Layout/Footer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import cn from 'src/utilities/cn';
import type { PageContextType } from './Layout';
import { useLayoutContext } from 'src/contexts/layout-context';
import Button from 'src/components/ui/Button';
import ThemeToggle from './ThemeToggle';
import { HandRaisedIcon, HandThumbDownIcon, HandThumbUpIcon } from '@heroicons/react/24/outline';
import {
HandRaisedIcon as HandRaisedSolidIcon,
Expand Down Expand Up @@ -329,11 +330,14 @@ const Footer: React.FC<{ pageContext: PageContextType }> = ({ pageContext }) =>
</div>
))}
</div>
<Status
additionalCSS="px-4 py-3 rounded-lg border border-neutral-300 dark:border-neutral-1000 hover:border-neutral-500 dark:hover:border-neutral-800 transition-colors"
statusUrl={StatusUrl}
showDescription
/>
<div className="flex flex-col items-end gap-3">
<Status
additionalCSS="px-4 py-3 rounded-lg border border-neutral-300 dark:border-neutral-1000 hover:border-neutral-500 dark:hover:border-neutral-800 transition-colors"
statusUrl={StatusUrl}
showDescription
/>
<ThemeToggle />
</div>
</div>
</footer>
);
Expand Down
3 changes: 2 additions & 1 deletion src/components/Layout/Header.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,8 @@ describe('Header', () => {

it('renders the header with logo and links', () => {
render(<Header />);
expect(screen.getByAltText('Ably')).toBeInTheDocument();
// Two logo variants render (light + dark, CSS-toggled); both carry the alt.
expect(screen.getAllByAltText('Ably').length).toBeGreaterThan(0);

expect(screen.getByText('Docs')).toBeInTheDocument();
expect(screen.getByText('Platform')).toBeInTheDocument();
Expand Down
4 changes: 3 additions & 1 deletion src/components/Layout/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import cn from 'src/utilities/cn';
import Icon from 'src/components/Icon';
import TabMenu from 'src/components/ui/TabMenu';
import Logo from 'src/images/ably-logo.svg';
import LogoDark from 'src/images/ably-logo-dark.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 Expand Up @@ -228,7 +229,8 @@ const Header: React.FC = () => {
<div className="flex items-center justify-between h-16 px-5 max-w-[1600px] mx-auto">
<div className="flex items-center shrink-0">
<a href="/docs" className="flex items-center gap-2 focus-base px-5 py-2 rounded -ml-5">
<img src={Logo} width="96px" alt="Ably" />
<img src={Logo} width="96px" alt="Ably" className="dark:hidden" />
<img src={LogoDark} width="96px" alt="Ably" className="hidden dark:block" />
<span className="bg-neutral-000 dark:bg-neutral-1300 border border-neutral-300 dark:border-neutral-1000 text-[10px] font-bold text-neutral-1000 dark:text-neutral-400 px-1.5 py-[3px] rounded-md uppercase">
Docs
</span>
Expand Down
57 changes: 57 additions & 0 deletions src/components/Layout/ThemeToggle.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import React, { useEffect, useState } from 'react';
import { ComputerDesktopIcon, MoonIcon, SunIcon } from '@heroicons/react/24/outline';
import { track } from '@ably/ui/core/insights';
import cn from 'src/utilities/cn';
import { useTheme } from 'src/contexts/theme-context';
import { DEFAULT_THEME, type Theme } from 'src/utilities/theme';

const OPTIONS: { value: Theme; label: string; icon: typeof SunIcon }[] = [
{ value: 'system', label: 'System theme', icon: ComputerDesktopIcon },
{ value: 'dark', label: 'Dark theme', icon: MoonIcon },
{ value: 'light', label: 'Light theme', icon: SunIcon },
];

const ThemeToggle: React.FC = () => {
const { theme, setTheme } = useTheme();
// `theme` comes from localStorage (client-only), so match the server's default
// until mounted to avoid a hydration mismatch on which cell is highlighted.
const [mounted, setMounted] = useState(false);
useEffect(() => setMounted(true), []);
const selected = mounted ? theme : DEFAULT_THEME;

return (
<div
role="radiogroup"
aria-label="Theme"
className="inline-flex items-center gap-0.5 p-0.5 rounded-lg border border-neutral-300 dark:border-neutral-1000"
>
{OPTIONS.map(({ value, label, icon: OptionIcon }) => {
const isActive = selected === value;
return (
<button
key={value}
type="button"
role="radio"
aria-checked={isActive}
aria-label={label}
title={label}
onClick={() => {
setTheme(value);
track('docs_theme_selected', { theme: value });
}}
className={cn(
'focus-base flex items-center justify-center size-8 rounded-md transition-colors',
isActive
? 'bg-neutral-100 dark:bg-neutral-1100 text-neutral-1300 dark:text-neutral-000'
: 'text-neutral-800 dark:text-neutral-500 hover:bg-neutral-100 dark:hover:bg-neutral-1200 hover:text-neutral-1300 dark:hover:text-neutral-000',
)}
>
<OptionIcon className="size-[18px]" aria-hidden />
</button>
);
})}
</div>
);
};

export default ThemeToggle;
4 changes: 3 additions & 1 deletion src/components/Layout/utils/styles.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import cn from 'src/utilities/cn';

// Tooltips stay a dark chip in both themes (dark surface, light text) rather
// than inverting to a light chip in dark mode.
export const tooltipContentClassName = cn(
'px-2 py-1 bg-neutral-1000 dark:bg-neutral-300 text-neutral-200 dark:text-neutral-1100 ui-text-p3 font-medium rounded-lg relative z-50 mt-2',
'px-2 py-1 bg-neutral-1000 dark:bg-neutral-1100 text-neutral-200 dark:text-neutral-200 ui-text-p3 font-medium rounded-lg relative z-50 mt-2',
'data-[state=closed]:animate-[tooltipExit_0.25s_ease-in-out]',
'data-[state=delayed-open]:animate-[tooltipEntry_0.25s_ease-in-out]',
);
Expand Down
Loading