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
2 changes: 1 addition & 1 deletion e2e-tests/explorer.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ test.describe('Explorer', () => {
});

test('Running tasks queue', async ({ page }) => {
const select = page.getByRole('combobox');
const select = page.getByTestId('example-select');
const option = page.getByRole('option', { name: 'tasks queue' });
const slider = page.getByRole('slider');
const script = page.getByText('script');
Expand Down
23 changes: 20 additions & 3 deletions index.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<!doctype html>
<html lang="en" class="dark" style="color-scheme: dark">
<html lang="en" class="dark" data-theme="midnight" style="color-scheme: dark">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/icon.svg" />
Expand All @@ -13,8 +13,25 @@
<script>
(function () {
try {
var theme = localStorage.getItem('theme');
if (theme === 'light') {
var stored = localStorage.getItem('theme');
var theme = stored;
if (stored === 'dark') theme = 'midnight';
else if (stored === 'light') theme = 'paper';
var lightThemes = { paper: 1, github: 1 };
var known = {
midnight: 1,
paper: 1,
nord: 1,
dracula: 1,
monokai: 1,
catppuccin: 1,
solarized: 1,
github: 1,
};
if (!theme || !known[theme]) theme = 'midnight';
var isLight = !!lightThemes[theme];
document.documentElement.setAttribute('data-theme', theme);
if (isLight) {
document.documentElement.classList.remove('dark');
document.documentElement.style.colorScheme = 'light';
} else {
Expand Down
10 changes: 5 additions & 5 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ export default function App() {
return (
<div className="flex flex-col grow min-h-screen">
<Header />
<div className="grow grid grid-cols-2 md:grid-cols-5 p-4 gap-4 lg:p-6 lg:gap-6">
<div className="col-span-2 h-full grid grid-rows-5 gap-4 lg:gap-6">
<main className="grow grid grid-cols-2 md:grid-cols-5 p-3 gap-3 sm:p-4 sm:gap-4 lg:p-6 lg:gap-5 xl:gap-6">
<div className="col-span-2 h-full grid grid-rows-5 gap-3 sm:gap-4 lg:gap-5 xl:gap-6">
<div className="row-span-3 flex overflow-auto">
<Configurator />
</div>
Expand All @@ -24,15 +24,15 @@ export default function App() {
<RequestAnimationFrame />
</div>
</div>
<div className="col-span-2 md:col-span-1 h-full grid grid-rows-2 gap-4 lg:gap-6">
<div className="col-span-2 md:col-span-1 h-full grid grid-rows-2 gap-3 sm:gap-4 lg:gap-5 xl:gap-6">
<div className="row-span-1 flex overflow-auto">
<Callstack />
</div>
<div className="row-span-1 flex overflow-auto">
<Console />
</div>
</div>
<div className="col-span-2 h-full grid grid-rows-5 gap-4 lg:gap-6">
<div className="col-span-2 h-full grid grid-rows-5 gap-3 sm:gap-4 lg:gap-5 xl:gap-6">
<div className="row-span-1 flex overflow-auto">
<TasksQueue />
</div>
Expand All @@ -43,7 +43,7 @@ export default function App() {
<EventLoop />
</div>
</div>
</div>
</main>
</div>
);
}
4 changes: 2 additions & 2 deletions src/components/card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@ export function Card({ className, text }: CardProps) {
return (
<div
className={cn(
'bg-[var(--color-secondary)] border rounded-lg grow',
'bg-secondary border border-border/70 rounded-lg grow animate-in fade-in-0 slide-in-from-bottom-1 duration-200',
className
)}
>
<div className="flex grow h-full items-center justify-center p-2 break-all">
<div className="flex grow h-full items-center justify-center p-2 break-all text-sm">
{text}
</div>
</div>
Expand Down
20 changes: 14 additions & 6 deletions src/components/chadcdn/dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,10 @@ function DialogTitle({
return (
<DialogPrimitive.Title
data-slot="dialog-title"
className={cn('text-lg leading-none font-semibold', className)}
className={cn(
'text-xl md:text-2xl leading-snug font-semibold tracking-tight',
className
)}
{...props}
/>
);
Expand All @@ -111,11 +114,16 @@ function DialogDescription({
...props
}: React.ComponentProps<typeof DialogPrimitive.Description>) {
return (
<DialogPrimitive.Description
data-slot="dialog-description"
className={cn('text-muted-foreground text-sm', className)}
{...props}
/>
<DialogPrimitive.Description asChild>
<div
data-slot="dialog-description"
className={cn(
'text-muted-foreground text-base md:text-lg leading-relaxed space-y-3 [&_p]:mb-0 [&_p]:text-inherit [&_li]:text-inherit',
className
)}
{...props}
/>
</DialogPrimitive.Description>
);
}

Expand Down
13 changes: 8 additions & 5 deletions src/components/infoContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,26 +19,29 @@ export function InfoContainer({
title,
}: InfoContainerProps) {
return (
<div
<section
data-testid="info-container"
className={cn(
'bg-[var(--color-card)] border rounded-lg p-2 lg:p-4 grow flex flex-col gap-2 lg:gap-4 max-w-full min-h-[10vh] md:min-h-0',
'bg-card text-card-foreground border border-border/80 rounded-xl p-3 lg:p-4 grow flex flex-col gap-2.5 lg:gap-4 max-w-full min-h-[10vh] md:min-h-0 shadow-[var(--shadow-panel)] transition-[background-color,border-color,box-shadow,color] duration-200',
className
)}
>
<div className="flex items-center justify-between gap-2">
<h3>{title}</h3>
<h3 className="text-sm font-semibold tracking-tight lg:text-base">
{title}
</h3>
<Modal title={title} description={description}>
<Button
variant="ghost"
size="iconSmall"
data-testid="info-container-button"
className="text-muted-foreground hover:text-foreground"
>
<Info className="size-5" />
<Info className="size-4 lg:size-5" />
</Button>
</Modal>
</div>
{children}
</div>
</section>
);
}
6 changes: 3 additions & 3 deletions src/components/progressCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,11 @@
}
};
checkProgress();
}, []);

Check warning on line 34 in src/components/progressCard.tsx

View workflow job for this annotation

GitHub Actions / check

React Hook useEffect has missing dependencies: 'delay' and 'end'. Either include them or remove the dependency array

return (
<div
className={`relative flex items-center p-1 flex-1 rounded-lg`}
className="relative flex items-center p-1 flex-1 rounded-lg animate-in fade-in-0 slide-in-from-bottom-1 duration-200"
style={{
backgroundImage: `conic-gradient(
currentColor,
Expand All @@ -46,11 +46,11 @@
>
<div
className={cn(
'bg-[var(--color-secondary)] border rounded-lg grow h-full',
'bg-secondary border border-border/70 rounded-lg grow h-full',
className
)}
>
<div className="flex grow h-full items-center justify-center p-2 break-all">
<div className="flex grow h-full items-center justify-center p-2 break-all text-sm">
{text}
</div>
</div>
Expand Down
23 changes: 16 additions & 7 deletions src/features/header/header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,25 +18,34 @@ function GitHubIcon({ className }: { className?: string }) {

export function Header() {
return (
<div className="flex px-4 py-2 lg:px-6 lg:py-4 items-center justify-between border-b">
<div className="flex items-center gap-2">
<Repeat />
<h2>Event loop explorer</h2>
<header className="flex shrink-0 items-center justify-between gap-3 border-b border-border/80 px-4 py-3 lg:px-6 lg:py-4 backdrop-blur-sm bg-background/70 transition-[background-color,border-color] duration-200">
<div className="flex items-center gap-3 min-w-0">
<span className="flex size-10 shrink-0 items-center justify-center self-center rounded-lg bg-secondary text-foreground">
<Repeat className="size-5" aria-hidden="true" />
</span>
<div className="flex min-w-0 flex-col justify-center leading-none">
<h2 className="m-0 truncate text-lg font-semibold leading-tight tracking-tight md:text-xl">
Event loop explorer
</h2>
<p className="m-0 mt-1 hidden text-xs leading-tight text-muted-foreground sm:block">
Step through the browser event loop
</p>
</div>
</div>

<div className="flex items-center gap-2">
<div className="flex items-center gap-1">
<Button asChild variant="ghost" size="iconBig">
<a
href="https://github.com/vault-developer/event-loop-explorer"
target="_blank"
rel="noreferrer"
aria-label="GitHub repository"
>
<GitHubIcon className="size-6" />
<GitHubIcon className="size-5" />
</a>
</Button>
<ThemeToggle />
</div>
</div>
</header>
);
}
68 changes: 41 additions & 27 deletions src/features/header/themeToggle.test.tsx
Original file line number Diff line number Diff line change
@@ -1,49 +1,63 @@
import { render, screen, fireEvent, cleanup } from '@testing-library/react';
import { afterEach, describe, expect, it, vi, type Mock } from 'vitest';
import {
afterEach,
beforeAll,
describe,
expect,
it,
vi,
type Mock,
} from 'vitest';
import { ThemeToggle } from './themeToggle';
import { useTheme } from '@/theme';
import { useTheme, THEMES } from '@/theme';

vi.mock('@/theme', () => ({
useTheme: vi.fn(),
}));
vi.mock('@/theme', async (importOriginal) => {
const actual = await importOriginal<typeof import('@/theme')>();
return {
...actual,
useTheme: vi.fn(),
};
});

beforeAll(() => {
Element.prototype.scrollIntoView = vi.fn();
HTMLElement.prototype.hasPointerCapture = vi.fn();
HTMLElement.prototype.releasePointerCapture = vi.fn();
HTMLElement.prototype.setPointerCapture = vi.fn();
});

afterEach(cleanup);

describe('ThemeToggle', () => {
it('should render Sun and Moon icons', () => {
it('should render the current theme label', () => {
(useTheme as Mock).mockReturnValue({
theme: 'dark',
theme: 'midnight',
setTheme: vi.fn(),
themeDefinition: THEMES.midnight,
});
render(<ThemeToggle />);

const moonIcon = screen.getByTestId('moon-icon');
const sunIcon = screen.getByTestId('sun-icon');
expect(moonIcon).toBeInTheDocument();
expect(sunIcon).toBeInTheDocument();
expect(screen.getByTestId('theme-toggle')).toBeInTheDocument();
expect(screen.getByLabelText('Select theme')).toBeInTheDocument();
expect(screen.getByText('Midnight')).toBeInTheDocument();
});

it('should toggle light theme on button click', () => {
it('should list theme presets and call setTheme on select', async () => {
const setThemeMock = vi.fn();
(useTheme as Mock).mockReturnValue({
theme: 'light',
theme: 'midnight',
setTheme: setThemeMock,
themeDefinition: THEMES.midnight,
});
render(<ThemeToggle />);
const toggleButton = screen.getByRole('button', { name: 'Toggle theme' });
fireEvent.click(toggleButton);
expect(setThemeMock).toHaveBeenCalledWith('dark');
});

it('should toggle dark theme on button click', () => {
const setThemeMock = vi.fn();
(useTheme as Mock).mockReturnValue({
theme: 'dark',
setTheme: setThemeMock,
});
render(<ThemeToggle />);
const toggleButton = screen.getByRole('button', { name: 'Toggle theme' });
fireEvent.click(toggleButton);
expect(setThemeMock).toHaveBeenCalledWith('light');
const trigger = screen.getByLabelText('Select theme');
fireEvent.pointerDown(trigger);
fireEvent.keyDown(trigger, { key: 'Enter' });

const nordOption = await screen.findByRole('option', { name: /Nord/i });
fireEvent.click(nordOption);

expect(setThemeMock).toHaveBeenCalledWith('nord');
});
});
66 changes: 43 additions & 23 deletions src/features/header/themeToggle.tsx
Original file line number Diff line number Diff line change
@@ -1,29 +1,49 @@
import { useTheme } from '@/theme';
import { Button } from '@/components/chadcdn/button';
import { Moon, Sun } from 'lucide-react';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
} from '@/components/chadcdn/select';
import { THEME_IDS, THEMES, useTheme, type ThemeId } from '@/theme';

function ThemeSwatch({ colors }: { colors: [string, string, string] }) {
return (
<span className="flex items-center gap-0.5" aria-hidden="true">
{colors.map((color) => (
<span
key={color}
className="size-2 rounded-full"
style={{ backgroundColor: color }}
/>
))}
</span>
);
}

export function ThemeToggle() {
const { setTheme, theme } = useTheme();
const onToggle = () => setTheme(theme === 'light' ? 'dark' : 'light');
const { theme, setTheme } = useTheme();

return (
<Button
variant="ghost"
size="iconBig"
onClick={onToggle}
data-testid="theme-toggle"
>
<Sun
data-testid="sun-icon"
className="absolute scale-100 dark:scale-0 rotate-0 dark:rotate-90 transition-all size-6"
size={24}
/>
<Moon
data-testid="moon-icon"
className="absolute scale-0 dark:scale-100 rotate-90 dark:rotate-0 transition-all size-6"
size={24}
/>
<span className="sr-only">Toggle theme</span>
</Button>
<Select value={theme} onValueChange={(value) => setTheme(value as ThemeId)}>
<SelectTrigger
size="sm"
data-testid="theme-toggle"
aria-label="Select theme"
className="min-w-0 gap-2 border-transparent bg-transparent shadow-none hover:bg-accent dark:hover:bg-accent/50 transition-colors"
>
<ThemeSwatch colors={THEMES[theme].swatch} />
<span className="hidden sm:inline">{THEMES[theme].label}</span>
</SelectTrigger>
<SelectContent align="end" className="min-w-[11rem]">
{THEME_IDS.map((id) => (
<SelectItem key={id} value={id}>
<span className="flex items-center gap-2">
<ThemeSwatch colors={THEMES[id].swatch} />
{THEMES[id].label}
</span>
</SelectItem>
))}
</SelectContent>
</Select>
);
}
Loading
Loading