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
10 changes: 7 additions & 3 deletions apps/framework-editor/app/components/HeaderFrameworks.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Skeleton } from '@trycompai/ui/skeleton';
import Link from 'next/link';
import { Suspense } from 'react';
import { ThemeToggle } from './theme-toggle';
import { UserMenu } from './user-menu';

export async function Header() {
Expand All @@ -9,9 +10,12 @@ export async function Header() {
<Link href="/frameworks" className="text-foreground hover:text-foreground/80 text-sm font-semibold tracking-tight">
Framework Editor
</Link>
<Suspense fallback={<Skeleton className="h-8 w-8 rounded-full" />}>
<UserMenu />
</Suspense>
<div className="flex items-center gap-2">
<ThemeToggle />
<Suspense fallback={<Skeleton className="h-8 w-8 rounded-full" />}>
<UserMenu />
</Suspense>
</div>
</header>
);
}
14 changes: 14 additions & 0 deletions apps/framework-editor/app/components/theme-provider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
'use client';

import { ThemeProvider as NextThemesProvider } from 'next-themes';
import * as React from 'react';

type ThemeProviderProps = Parameters<typeof NextThemesProvider>[0];

export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
return (
<NextThemesProvider {...props}>
<React.Suspense>{children}</React.Suspense>
</NextThemesProvider>
);
}
41 changes: 41 additions & 0 deletions apps/framework-editor/app/components/theme-toggle.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { fireEvent, render, screen } from '@testing-library/react';
import { beforeEach, describe, expect, it, vi } from 'vitest';

const { setTheme, useThemeMock } = vi.hoisted(() => ({
setTheme: vi.fn(),
useThemeMock: vi.fn(),
}));

vi.mock('next-themes', () => ({ useTheme: useThemeMock }));
vi.mock('@trycompai/ui', () => ({
Button: ({
children,
variant: _v,
size: _s,
...props
}: { variant?: string; size?: string } & React.ComponentProps<'button'>) => (
<button {...props}>{children}</button>
),
}));

import { ThemeToggle } from './theme-toggle';

describe('ThemeToggle', () => {
beforeEach(() => {
vi.clearAllMocks();
useThemeMock.mockReturnValue({ resolvedTheme: 'light', setTheme });
});

it('switches to dark when currently light', () => {
render(<ThemeToggle />);
fireEvent.click(screen.getByRole('button', { name: /toggle theme/i }));
expect(setTheme).toHaveBeenCalledWith('dark');
});

it('switches to light when currently dark', () => {
useThemeMock.mockReturnValue({ resolvedTheme: 'dark', setTheme });
render(<ThemeToggle />);
fireEvent.click(screen.getByRole('button', { name: /toggle theme/i }));
expect(setTheme).toHaveBeenCalledWith('light');
});
});
30 changes: 30 additions & 0 deletions apps/framework-editor/app/components/theme-toggle.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
'use client';

import { Button } from '@trycompai/ui';
import { Moon, Sun } from 'lucide-react';
import { useTheme } from 'next-themes';
import { useEffect, useState } from 'react';

export function ThemeToggle() {
const { resolvedTheme, setTheme } = useTheme();
const [mounted, setMounted] = useState(false);

// next-themes can't know the resolved theme during SSR; wait for mount before
// showing a theme-specific icon to avoid a hydration mismatch.
useEffect(() => setMounted(true), []);

const isDark = mounted && resolvedTheme === 'dark';

return (
<Button
variant="ghost"
size="icon"
aria-label="Toggle theme"
title={isDark ? 'Switch to light mode' : 'Switch to dark mode'}
className="h-8 w-8"
onClick={() => setTheme(isDark ? 'light' : 'dark')}
>
{isDark ? <Sun className="h-4 w-4" /> : <Moon className="h-4 w-4" />}
</Button>
);
}
22 changes: 15 additions & 7 deletions apps/framework-editor/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { Toaster as SonnerToaster } from 'sonner';
import { headers } from 'next/headers';
import '../styles/globals.css';
import { Header } from './components/HeaderFrameworks';
import { ThemeProvider } from './components/theme-provider';
import { auth } from './lib/auth';
import { isInternalUser } from './lib/utils';

Expand All @@ -25,14 +26,21 @@ export default async function RootLayout({ children }: { children: ReactNode })
isInternalUser(session.user.email);

return (
<html lang="en" className="h-full">
<html lang="en" className="h-full" suppressHydrationWarning>
<body className="flex h-full flex-col">
{hasSession && <Header />}
<div className="flex min-h-0 flex-1 flex-col gap-2 p-4">
{children}
<Toaster />
<SonnerToaster richColors position="top-right" />
</div>
<ThemeProvider
attribute="class"
defaultTheme="light"
enableSystem
disableTransitionOnChange
>
{hasSession && <Header />}
<div className="flex min-h-0 flex-1 flex-col gap-2 p-4">
{children}
<Toaster />
<SonnerToaster richColors position="top-right" />
</div>
</ThemeProvider>
</body>
</html>
);
Expand Down
1 change: 1 addition & 0 deletions apps/framework-editor/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"framer-motion": "^12.23.9",
"lucide-react": "^1.7.0",
"next": "^16.2.0",
"next-themes": "^0.4.4",
"nuqs": "^2.4.3",
"react": "^19.0.0",
"react-dom": "^19.0.0",
Expand Down
30 changes: 30 additions & 0 deletions apps/framework-editor/styles/globals.css
Original file line number Diff line number Diff line change
@@ -1,6 +1,36 @@
@import '@trycompai/ui/globals.css';
@config '../tailwind.config.ts';

/*
* Real dark-mode palette (FRAME-5). @trycompai/ui ships a `.dark` block whose
* values are identical to light, so without this, toggling the `dark` class
* changes nothing. Declared after the import (and unlayered) so it wins the
* cascade over the library defaults.
*/
.dark {
--background: 0 0% 7%;
--foreground: 0 0% 95%;
--card: 0 0% 9%;
--card-foreground: 0 0% 95%;
--popover: 0 0% 9%;
--popover-foreground: 0 0% 95%;
--primary: 165 70% 42%;
--primary-foreground: 0 0% 100%;
--secondary: 0 0% 15%;
--secondary-foreground: 0 0% 95%;
--muted: 0 0% 15%;
--muted-foreground: 0 0% 64%;
--accent: 0 0% 18%;
--accent-foreground: 0 0% 98%;
--destructive: 0 72% 51%;
--destructive-foreground: 0 0% 98%;
--warning: 45 93% 47%;
--warning-foreground: 26 83% 14%;
--border: 0 0% 18%;
--input: 0 0% 18%;
--ring: 165 70% 42%;
}

@layer base {
* {
@apply border-border;
Expand Down
1 change: 1 addition & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading