feat(frontend): visual QA pass — unified page titles, CTAs, native controls, 404 fixes
- add PageTitle and apply it on /download, /themes, /configs, /showcase, /link, 404 - reset range-slider thumbs in the theme editor; fold color picker into the swatch - hide horizontal scrollbars in the ClickGui mock; clip html to kill 100vw pan - pin the footer under short pages via flex AppShell; split 404 art from caption - one landing CTA geometry (lv-cta-lg / quiet); CLOUD eyebrow; auth caption scrim - showcase error hides sort/pager; query retry limited to 1
This commit is contained in:
parent
10d90dce93
commit
7bb981b58e
26 changed files with 198 additions and 73 deletions
|
|
@ -7,7 +7,7 @@ import { TopBar } from './topbar/TopBar'
|
|||
export function AppShell() {
|
||||
const { t } = useTranslation('common')
|
||||
return (
|
||||
<>
|
||||
<div className="flex min-h-dvh flex-col">
|
||||
<a
|
||||
href="#main"
|
||||
className="sr-only focus:not-sr-only focus:absolute focus:left-4 focus:top-2 focus:z-50 focus:rounded-md focus:bg-slate focus:px-3 focus:py-2"
|
||||
|
|
@ -16,10 +16,10 @@ export function AppShell() {
|
|||
</a>
|
||||
<KonamiEffect />
|
||||
<TopBar />
|
||||
<main id="main" className="mx-auto w-full max-w-[1120px] px-4 pt-(--topbar-h) sm:px-6">
|
||||
<main id="main" className="mx-auto flex w-full max-w-[1120px] flex-1 flex-col px-4 pt-(--topbar-h) sm:px-6">
|
||||
<Outlet />
|
||||
</main>
|
||||
<Footer />
|
||||
</>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import type { ButtonHTMLAttributes } from 'react'
|
||||
import type { AnchorHTMLAttributes, ButtonHTMLAttributes } from 'react'
|
||||
import { Link, type LinkProps } from 'react-router'
|
||||
|
||||
type Variant = 'primary' | 'quiet' | 'danger'
|
||||
|
|
@ -35,3 +35,12 @@ export function Button({
|
|||
export function ButtonLink({ variant = 'primary', className = '', ...rest }: LinkProps & { variant?: Variant }) {
|
||||
return <Link className={`${BASE} ${VARIANTS[variant]} ${className}`} {...rest} />
|
||||
}
|
||||
|
||||
/** Like ButtonLink but escapes to a plain <a> for external/href CTAs (e.g. GitHub downloads). */
|
||||
export function ButtonAnchor({
|
||||
variant = 'primary',
|
||||
className = '',
|
||||
...rest
|
||||
}: AnchorHTMLAttributes<HTMLAnchorElement> & { variant?: Variant }) {
|
||||
return <a className={`${BASE} ${VARIANTS[variant]} ${className}`} {...rest} />
|
||||
}
|
||||
|
|
|
|||
16
frontend/src/shared/ui/PageTitle.tsx
Normal file
16
frontend/src/shared/ui/PageTitle.tsx
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import type { ReactNode } from 'react'
|
||||
|
||||
/**
|
||||
* The canonical page heading. Every top-level page (except the landing hero and the
|
||||
* auth panes) uses this so titles share one size, weight and uppercase treatment.
|
||||
*/
|
||||
export function PageTitle({ children, lead }: { children: ReactNode; lead?: ReactNode }) {
|
||||
return (
|
||||
<header className="flex flex-col gap-2">
|
||||
<h1 className="font-display text-3xl font-black uppercase leading-tight tracking-[0.01em] text-snow">
|
||||
{children}
|
||||
</h1>
|
||||
{lead ? <p className="max-w-[60ch] text-frost">{lead}</p> : null}
|
||||
</header>
|
||||
)
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@ import { render, screen } from '@testing-library/react'
|
|||
import userEvent from '@testing-library/user-event'
|
||||
import { expect, test, vi } from 'vitest'
|
||||
import { Button } from '../Button'
|
||||
import { PageTitle } from '../PageTitle'
|
||||
import { TextField } from '../TextField'
|
||||
import { ShareCode } from '../ShareCode'
|
||||
import { Notice } from '../Notice'
|
||||
|
|
@ -34,3 +35,11 @@ test('error notice is an alert', () => {
|
|||
render(<Notice tone="error">Неверный код</Notice>)
|
||||
expect(screen.getByRole('alert')).toHaveTextContent('Неверный код')
|
||||
})
|
||||
|
||||
test('page title renders one h1 with an optional lead', () => {
|
||||
render(
|
||||
<PageTitle lead="Короткое описание">Мои конфиги</PageTitle>,
|
||||
)
|
||||
expect(screen.getByRole('heading', { level: 1, name: 'Мои конфиги' })).toBeInTheDocument()
|
||||
expect(screen.getByText('Короткое описание')).toBeInTheDocument()
|
||||
})
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue