Public Access
feat(appearance): accent color, corners, wide layout and game art background
This commit is contained in:
1 parent
91d5672421
commit
2c360c9421
28 files changed
+934
-46
No files matched your search
@@ -12,6 +12,7 @@ import {
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
import {usePreferences} from '@/context/PreferencesContext.jsx';
|
||||
import {cn} from '@/lib/utils';
|
||||
import PageContainer from './PageContainer.jsx';
|
||||
|
||||
const NAV_ITEMS = [
|
||||
{to: '/dashboard', labelKey: 'NAV.DASHBOARD', Icon: LayoutDashboard},
|
||||
@@ -37,7 +38,7 @@ export default function AppHeader({isAuthenticated, onLogout}) {
|
||||
|
||||
return (
|
||||
<header className="sticky top-0 z-50 border-b bg-background/80 backdrop-blur supports-[backdrop-filter]:bg-background/60">
|
||||
<div className="mx-auto flex h-16 max-w-5xl items-center gap-2 px-4 md:px-8">
|
||||
<PageContainer className="flex h-16 items-center gap-2 px-4 md:px-8">
|
||||
<Link to={isAuthenticated ? '/dashboard' : '/'} className="mr-2 flex items-center gap-2 font-bold tracking-tight whitespace-nowrap">
|
||||
<span className="flex size-8 items-center justify-center rounded-lg bg-primary text-primary-foreground">
|
||||
<Gamepad2 className="size-5"/>
|
||||
@@ -95,7 +96,7 @@ export default function AppHeader({isAuthenticated, onLogout}) {
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</PageContainer>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import {useState} from 'react';
|
||||
import {coverUrl, heroUrl} from '../utils/games.js';
|
||||
|
||||
/**
|
||||
* Game art behind the whole page, blurred and dimmed so text on it stays readable. Falls back to the store
|
||||
* header for apps without library art. Give it `key={appid}`, so a new game starts with its library art again.
|
||||
*/
|
||||
export default function PageBackground({appid}) {
|
||||
const [failures, setFailures] = useState(0);
|
||||
|
||||
if (!appid || failures > 1) return null;
|
||||
|
||||
return (
|
||||
<div aria-hidden="true" className="pointer-events-none fixed inset-0 -z-10 overflow-hidden">
|
||||
<img src={failures === 0 ? heroUrl(appid) : coverUrl(appid)} alt=""
|
||||
onError={() => setFailures((count) => count + 1)}
|
||||
className="size-full scale-110 object-cover blur-md"/>
|
||||
<div className="absolute inset-0 bg-background/80"/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
// @vitest-environment jsdom
|
||||
import {afterEach, describe, expect, it} from 'vitest';
|
||||
import {cleanup, fireEvent, render} from '@testing-library/react';
|
||||
import PageBackground from './PageBackground.jsx';
|
||||
|
||||
afterEach(cleanup);
|
||||
|
||||
describe('PageBackground', () => {
|
||||
it('shows nothing without a game', () => {
|
||||
const {container} = render(<PageBackground appid={null}/>);
|
||||
|
||||
expect(container.innerHTML).toBe('');
|
||||
});
|
||||
|
||||
it('shows the library art, then the store header, then nothing', () => {
|
||||
const {container} = render(<PageBackground appid={620}/>);
|
||||
const img = () => container.querySelector('img');
|
||||
expect(img().getAttribute('src')).toContain('/620/library_hero.jpg');
|
||||
|
||||
fireEvent.error(img());
|
||||
expect(img().getAttribute('src')).toContain('/620/header.jpg');
|
||||
|
||||
fireEvent.error(img());
|
||||
expect(container.innerHTML).toBe('');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,14 @@
|
||||
import {cn} from '@/lib/utils';
|
||||
import {usePreferences} from '../context/PreferencesContext.jsx';
|
||||
|
||||
/**
|
||||
* The centered page column. Its width follows the wide layout setting.
|
||||
*/
|
||||
export default function PageContainer({className, children}) {
|
||||
const {appearance} = usePreferences();
|
||||
return (
|
||||
<div className={cn('w-full mx-auto', appearance.wideLayout ? 'max-w-7xl' : 'max-w-5xl', className)}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
// @vitest-environment jsdom
|
||||
import {afterEach, describe, expect, it, vi} from 'vitest';
|
||||
import {cleanup, render} from '@testing-library/react';
|
||||
import {PreferencesProvider} from '../context/PreferencesContext.jsx';
|
||||
import PageContainer from './PageContainer.jsx';
|
||||
|
||||
vi.mock('react-i18next', () => import('../test/i18n-mock.js'));
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
localStorage.clear();
|
||||
});
|
||||
|
||||
const renderContainer = () => render(
|
||||
<PreferencesProvider><PageContainer className="p-4">content</PageContainer></PreferencesProvider>,
|
||||
).container.firstChild;
|
||||
|
||||
describe('PageContainer', () => {
|
||||
it('is the normal column by default', () => {
|
||||
expect(renderContainer().className).toContain('max-w-5xl');
|
||||
});
|
||||
|
||||
it('is wider with the wide layout', () => {
|
||||
localStorage.setItem('tbp_appearance', JSON.stringify({wideLayout: true}));
|
||||
|
||||
const container = renderContainer();
|
||||
|
||||
expect(container.className).toContain('max-w-7xl');
|
||||
expect(container.className).toContain('p-4');
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user